
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.term; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.context.support.ConceptValidationOptions; 025import ca.uhn.fhir.context.support.IValidationSupport; 026import ca.uhn.fhir.context.support.LookupCodeRequest; 027import ca.uhn.fhir.context.support.ValidationSupportContext; 028import ca.uhn.fhir.context.support.ValueSetExpansionOptions; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.interceptor.model.RequestPartitionId; 031import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 032import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 033import ca.uhn.fhir.jpa.api.dao.IDao; 034import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 035import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; 036import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 037import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 038import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; 039import ca.uhn.fhir.jpa.config.util.ConnectionPoolInfoProvider; 040import ca.uhn.fhir.jpa.config.util.IConnectionPoolInfoProvider; 041import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; 042import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 043import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; 044import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; 045import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; 046import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao; 047import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao; 048import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewDao; 049import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewOracleDao; 050import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao; 051import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 052import ca.uhn.fhir.jpa.entity.ITermValueSetConceptView; 053import ca.uhn.fhir.jpa.entity.TermCodeSystem; 054import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; 055import ca.uhn.fhir.jpa.entity.TermConcept; 056import ca.uhn.fhir.jpa.entity.TermConceptDesignation; 057import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; 058import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; 059import ca.uhn.fhir.jpa.entity.TermConceptProperty; 060import ca.uhn.fhir.jpa.entity.TermConceptPropertyTypeEnum; 061import ca.uhn.fhir.jpa.entity.TermValueSet; 062import ca.uhn.fhir.jpa.entity.TermValueSetConcept; 063import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; 064import ca.uhn.fhir.jpa.model.config.PartitionSettings; 065import ca.uhn.fhir.jpa.model.dao.JpaPid; 066import ca.uhn.fhir.jpa.model.entity.ResourceTable; 067import ca.uhn.fhir.jpa.model.sched.HapiJob; 068import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs; 069import ca.uhn.fhir.jpa.model.sched.ISchedulerService; 070import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; 071import ca.uhn.fhir.jpa.model.util.JpaConstants; 072import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 073import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; 074import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 075import ca.uhn.fhir.jpa.term.api.ReindexTerminologyResult; 076import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException; 077import ca.uhn.fhir.rest.api.Constants; 078import ca.uhn.fhir.rest.api.server.RequestDetails; 079import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 080import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 081import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 082import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 083import ca.uhn.fhir.util.CoverageIgnore; 084import ca.uhn.fhir.util.FhirVersionIndependentConcept; 085import ca.uhn.fhir.util.HapiExtensions; 086import ca.uhn.fhir.util.StopWatch; 087import ca.uhn.fhir.util.UrlUtil; 088import ca.uhn.fhir.util.ValidateUtil; 089import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 090import com.google.common.annotations.VisibleForTesting; 091import com.google.common.base.Stopwatch; 092import com.google.common.collect.ArrayListMultimap; 093import jakarta.annotation.Nonnull; 094import jakarta.annotation.Nullable; 095import jakarta.annotation.PostConstruct; 096import jakarta.persistence.EntityManager; 097import jakarta.persistence.NonUniqueResultException; 098import jakarta.persistence.PersistenceContext; 099import jakarta.persistence.PersistenceContextType; 100import org.apache.commons.collections4.ListUtils; 101import org.apache.commons.lang3.ObjectUtils; 102import org.apache.commons.lang3.StringUtils; 103import org.apache.commons.lang3.Validate; 104import org.apache.commons.lang3.time.DateUtils; 105import org.apache.lucene.index.Term; 106import org.apache.lucene.search.IndexSearcher; 107import org.hibernate.CacheMode; 108import org.hibernate.search.engine.search.predicate.dsl.BooleanPredicateClausesStep; 109import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; 110import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; 111import org.hibernate.search.engine.search.query.SearchQuery; 112import org.hibernate.search.engine.search.query.SearchScroll; 113import org.hibernate.search.engine.search.query.SearchScrollResult; 114import org.hibernate.search.mapper.orm.Search; 115import org.hibernate.search.mapper.orm.common.EntityReference; 116import org.hibernate.search.mapper.orm.session.SearchSession; 117import org.hibernate.search.mapper.pojo.massindexing.impl.PojoMassIndexingLoggingMonitor; 118import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; 119import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; 120import org.hl7.fhir.convertors.context.ConversionContext40_50; 121import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50; 122import org.hl7.fhir.convertors.conv40_50.resources40_50.ValueSet40_50; 123import org.hl7.fhir.instance.model.api.IAnyResource; 124import org.hl7.fhir.instance.model.api.IBaseCoding; 125import org.hl7.fhir.instance.model.api.IBaseDatatype; 126import org.hl7.fhir.instance.model.api.IBaseResource; 127import org.hl7.fhir.instance.model.api.IIdType; 128import org.hl7.fhir.instance.model.api.IPrimitiveType; 129import org.hl7.fhir.r4.model.BooleanType; 130import org.hl7.fhir.r4.model.CanonicalType; 131import org.hl7.fhir.r4.model.CodeSystem; 132import org.hl7.fhir.r4.model.CodeableConcept; 133import org.hl7.fhir.r4.model.Coding; 134import org.hl7.fhir.r4.model.DateTimeType; 135import org.hl7.fhir.r4.model.DecimalType; 136import org.hl7.fhir.r4.model.DomainResource; 137import org.hl7.fhir.r4.model.Enumerations; 138import org.hl7.fhir.r4.model.Extension; 139import org.hl7.fhir.r4.model.InstantType; 140import org.hl7.fhir.r4.model.IntegerType; 141import org.hl7.fhir.r4.model.StringType; 142import org.hl7.fhir.r4.model.ValueSet; 143import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome; 144import org.quartz.JobExecutionContext; 145import org.springframework.beans.factory.annotation.Autowired; 146import org.springframework.context.ApplicationContext; 147import org.springframework.data.domain.PageRequest; 148import org.springframework.data.domain.Pageable; 149import org.springframework.data.domain.Slice; 150import org.springframework.transaction.PlatformTransactionManager; 151import org.springframework.transaction.TransactionDefinition; 152import org.springframework.transaction.annotation.Propagation; 153import org.springframework.transaction.annotation.Transactional; 154import org.springframework.transaction.interceptor.NoRollbackRuleAttribute; 155import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute; 156import org.springframework.transaction.support.TransactionSynchronizationManager; 157import org.springframework.transaction.support.TransactionTemplate; 158import org.springframework.util.comparator.Comparators; 159 160import java.util.ArrayList; 161import java.util.Arrays; 162import java.util.Collection; 163import java.util.Collections; 164import java.util.Date; 165import java.util.HashMap; 166import java.util.HashSet; 167import java.util.LinkedHashMap; 168import java.util.List; 169import java.util.Map; 170import java.util.Objects; 171import java.util.Optional; 172import java.util.Set; 173import java.util.StringTokenizer; 174import java.util.UUID; 175import java.util.concurrent.TimeUnit; 176import java.util.function.Consumer; 177import java.util.function.Supplier; 178import java.util.stream.Collectors; 179 180import static ca.uhn.fhir.jpa.entity.TermConceptPropertyBinder.CONCEPT_PROPERTY_PREFIX_NAME; 181import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; 182import static java.lang.String.join; 183import static java.util.stream.Collectors.joining; 184import static java.util.stream.Collectors.toList; 185import static java.util.stream.Collectors.toSet; 186import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 187import static org.apache.commons.lang3.StringUtils.defaultString; 188import static org.apache.commons.lang3.StringUtils.isBlank; 189import static org.apache.commons.lang3.StringUtils.isEmpty; 190import static org.apache.commons.lang3.StringUtils.isNoneBlank; 191import static org.apache.commons.lang3.StringUtils.isNotBlank; 192import static org.apache.commons.lang3.StringUtils.lowerCase; 193import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase; 194 195public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { 196 public static final int DEFAULT_FETCH_SIZE = 250; 197 public static final int DEFAULT_MASS_INDEXER_OBJECT_LOADING_THREADS = 2; 198 // doesn't seem to be much gain by using more threads than this value 199 public static final int MAX_MASS_INDEXER_OBJECT_LOADING_THREADS = 6; 200 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TermReadSvcImpl.class); 201 private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); 202 private static final TermCodeSystemVersionDetails NO_CURRENT_VERSION = new TermCodeSystemVersionDetails(-1L, null); 203 private static final String OUR_PIPE_CHARACTER = "|"; 204 private static final int SECONDS_IN_MINUTE = 60; 205 private static final int INDEXED_ROOTS_LOGGING_COUNT = 50_000; 206 private static final String CS_USERDATA_CURRENT_VERSION = TermReadSvcImpl.class.getName() + "_CS_CURRENT_VERSION"; 207 private static final String VS_USERDATA_CURRENT_VERSION = TermReadSvcImpl.class.getName() + "_VS_CURRENT_VERSION"; 208 private static Runnable myInvokeOnNextCallForUnitTest; 209 private static boolean ourForceDisableHibernateSearchForUnitTest; 210 211 @Autowired 212 protected DaoRegistry myDaoRegistry; 213 214 @Autowired 215 protected ITermCodeSystemDao myCodeSystemDao; 216 217 @Autowired 218 protected ITermConceptDao myConceptDao; 219 220 @Autowired 221 protected ITermValueSetDao myTermValueSetDao; 222 223 @Autowired 224 protected ITermValueSetConceptDao myValueSetConceptDao; 225 226 @Autowired 227 protected ITermValueSetConceptDesignationDao myValueSetConceptDesignationDao; 228 229 @Autowired 230 protected FhirContext myContext; 231 232 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 233 protected EntityManager myEntityManager; 234 235 private boolean myPreExpandingValueSets = false; 236 237 @Autowired 238 private ITermCodeSystemVersionDao myCodeSystemVersionDao; 239 240 @Autowired 241 private JpaStorageSettings myStorageSettings; 242 243 private TransactionTemplate myTxTemplate; 244 245 @Autowired 246 private PlatformTransactionManager myTransactionManager; 247 248 @Autowired(required = false) 249 private IFulltextSearchSvc myFulltextSearchSvc; 250 251 @Autowired 252 private PlatformTransactionManager myTxManager; 253 254 @Autowired 255 private ITermConceptDao myTermConceptDao; 256 257 @Autowired 258 private ITermValueSetConceptViewDao myTermValueSetConceptViewDao; 259 260 @Autowired 261 private ITermValueSetConceptViewOracleDao myTermValueSetConceptViewOracleDao; 262 263 @Autowired(required = false) 264 private ITermDeferredStorageSvc myDeferredStorageSvc; 265 266 @Autowired 267 private IIdHelperService<JpaPid> myIdHelperService; 268 269 @Autowired 270 private ApplicationContext myApplicationContext; 271 272 private volatile IValidationSupport myJpaValidationSupport; 273 private volatile IValidationSupport myValidationSupport; 274 // We need this bean so we can tell which mode hibernate search is running in. 275 @Autowired 276 private HibernatePropertiesProvider myHibernatePropertiesProvider; 277 278 @Autowired 279 private VersionCanonicalizer myVersionCanonicalizer; 280 281 @Autowired 282 private IJpaStorageResourceParser myJpaStorageResourceParser; 283 284 @Autowired 285 private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport; 286 287 @Autowired 288 private ValueSetConceptAccumulatorFactory myValueSetConceptAccumulatorFactory; 289 290 @Autowired 291 private PartitionSettings myPartitionSettings; 292 293 @Override 294 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 295 if (isBlank(theSystem)) { 296 return false; 297 } 298 TermCodeSystemVersionDetails cs = getCurrentCodeSystemVersion(theValidationSupportContext, theSystem); 299 return cs != null; 300 } 301 302 @Override 303 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 304 return fetchValueSet(theValueSetUrl) != null; 305 } 306 307 private boolean addCodeIfNotAlreadyAdded( 308 @Nullable ValueSetExpansionOptions theExpansionOptions, 309 IValueSetConceptAccumulator theValueSetCodeAccumulator, 310 Set<String> theAddedCodes, 311 TermConcept theConcept, 312 String theConceptDisplayValue, 313 boolean theAdd, 314 String theValueSetIncludeVersion) { 315 String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri(); 316 String codeSystemVersion = theConcept.getCodeSystemVersion().getCodeSystemVersionId(); 317 String code = theConcept.getCode(); 318 Long sourceConceptPid = theConcept.getId(); 319 String directParentPids = ""; 320 321 if (theExpansionOptions != null && theExpansionOptions.isIncludeHierarchy()) { 322 directParentPids = theConcept.getParents().stream() 323 .map(t -> t.getParent().getId().toString()) 324 .collect(joining(" ")); 325 } 326 327 Collection<TermConceptDesignation> designations = theConcept.getDesignations(); 328 329 if (StringUtils.isNotEmpty(theValueSetIncludeVersion)) { 330 codeSystem = codeSystem + OUR_PIPE_CHARACTER + theValueSetIncludeVersion; 331 } 332 333 return addCodeIfNotAlreadyAdded( 334 theValueSetCodeAccumulator, 335 theAddedCodes, 336 designations, 337 theAdd, 338 codeSystem, 339 code, 340 theConceptDisplayValue, 341 sourceConceptPid, 342 directParentPids, 343 codeSystemVersion); 344 } 345 346 private boolean addCodeIfNotAlreadyAdded( 347 IValueSetConceptAccumulator theValueSetCodeAccumulator, 348 Set<String> theAddedCodes, 349 boolean theAdd, 350 String theCodeSystem, 351 String theCodeSystemVersion, 352 String theCode, 353 String theDisplay, 354 Long theSourceConceptPid, 355 String theSourceConceptDirectParentPids, 356 Collection<TermConceptDesignation> theDesignations) { 357 if (StringUtils.isNotEmpty(theCodeSystemVersion)) { 358 if (isNoneBlank(theCodeSystem, theCode)) { 359 if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 360 theValueSetCodeAccumulator.includeConceptWithDesignations( 361 theCodeSystem + OUR_PIPE_CHARACTER + theCodeSystemVersion, 362 theCode, 363 theDisplay, 364 theDesignations, 365 theSourceConceptPid, 366 theSourceConceptDirectParentPids, 367 theCodeSystemVersion); 368 return true; 369 } 370 371 if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 372 theValueSetCodeAccumulator.excludeConcept( 373 theCodeSystem + OUR_PIPE_CHARACTER + theCodeSystemVersion, theCode); 374 return true; 375 } 376 } 377 } else { 378 if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 379 theValueSetCodeAccumulator.includeConceptWithDesignations( 380 theCodeSystem, 381 theCode, 382 theDisplay, 383 theDesignations, 384 theSourceConceptPid, 385 theSourceConceptDirectParentPids, 386 theCodeSystemVersion); 387 return true; 388 } 389 390 if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 391 theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode); 392 return true; 393 } 394 } 395 396 return false; 397 } 398 399 private boolean addCodeIfNotAlreadyAdded( 400 IValueSetConceptAccumulator theValueSetCodeAccumulator, 401 Set<String> theAddedCodes, 402 Collection<TermConceptDesignation> theDesignations, 403 boolean theAdd, 404 String theCodeSystem, 405 String theCode, 406 String theDisplay, 407 Long theSourceConceptPid, 408 String theSourceConceptDirectParentPids, 409 String theSystemVersion) { 410 if (isNoneBlank(theCodeSystem, theCode)) { 411 if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 412 theValueSetCodeAccumulator.includeConceptWithDesignations( 413 theCodeSystem, 414 theCode, 415 theDisplay, 416 theDesignations, 417 theSourceConceptPid, 418 theSourceConceptDirectParentPids, 419 theSystemVersion); 420 return true; 421 } 422 423 if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) { 424 theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode); 425 return true; 426 } 427 } 428 429 return false; 430 } 431 432 private boolean addToSet(Set<TermConcept> theSetToPopulate, TermConcept theConcept) { 433 boolean retVal = theSetToPopulate.add(theConcept); 434 if (retVal) { 435 if (theSetToPopulate.size() >= myStorageSettings.getMaximumExpansionSize()) { 436 String msg = myContext 437 .getLocalizer() 438 .getMessage( 439 TermReadSvcImpl.class, 440 "expansionTooLarge", 441 myStorageSettings.getMaximumExpansionSize()); 442 throw new ExpansionTooCostlyException(Msg.code(885) + msg); 443 } 444 } 445 return retVal; 446 } 447 448 public Optional<TermValueSet> deleteValueSetForResource(ResourceTable theResourceTable) { 449 // Get existing entity so it can be deleted. 450 Optional<TermValueSet> optionalExistingTermValueSetById = 451 myTermValueSetDao.findByResourcePid(theResourceTable.getId()); 452 453 if (optionalExistingTermValueSetById.isPresent()) { 454 TermValueSet existingTermValueSet = optionalExistingTermValueSetById.get(); 455 456 ourLog.info("Deleting existing TermValueSet[{}] and its children...", existingTermValueSet.getId()); 457 deletePreCalculatedValueSetContents(existingTermValueSet); 458 myTermValueSetDao.deleteById(existingTermValueSet.getPartitionedId()); 459 460 /* 461 * If we're updating an existing ValueSet within a transaction, we need to make 462 * sure to manually flush now since otherwise we'll try to create a new 463 * TermValueSet entity and fail with a constraint error on the URL, since 464 * this one won't be deleted yet 465 */ 466 myTermValueSetDao.flush(); 467 468 ourLog.info("Done deleting existing TermValueSet[{}] and its children.", existingTermValueSet.getId()); 469 } 470 471 return optionalExistingTermValueSetById; 472 } 473 474 private void deletePreCalculatedValueSetContents(TermValueSet theValueSet) { 475 myValueSetConceptDesignationDao.deleteByTermValueSetId(theValueSet.getId()); 476 myValueSetConceptDao.deleteByTermValueSetId(theValueSet.getId()); 477 } 478 479 @Override 480 @Transactional 481 public void deleteValueSetAndChildren(ResourceTable theResourceTable) { 482 deleteValueSetForResource(theResourceTable); 483 } 484 485 @Override 486 @Transactional 487 public List<FhirVersionIndependentConcept> expandValueSetIntoConceptList( 488 @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull String theValueSetCanonicalUrl) { 489 // TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not be found 490 // in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the expansion may 491 // time-out. 492 493 ValueSet expanded = expandValueSet(theExpansionOptions, theValueSetCanonicalUrl); 494 495 ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(); 496 for (ValueSet.ValueSetExpansionContainsComponent nextContains : 497 expanded.getExpansion().getContains()) { 498 retVal.add(new FhirVersionIndependentConcept( 499 nextContains.getSystem(), 500 nextContains.getCode(), 501 nextContains.getDisplay(), 502 nextContains.getVersion())); 503 } 504 return retVal; 505 } 506 507 @Override 508 public ValueSet expandValueSet( 509 @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull String theValueSetCanonicalUrl) { 510 ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(theValueSetCanonicalUrl); 511 if (valueSet == null) { 512 throw new ResourceNotFoundException( 513 Msg.code(886) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSetCanonicalUrl)); 514 } 515 516 return expandValueSet(theExpansionOptions, valueSet); 517 } 518 519 @Override 520 public ValueSet expandValueSet( 521 @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull ValueSet theValueSetToExpand) { 522 String filter = null; 523 if (theExpansionOptions != null) { 524 filter = theExpansionOptions.getFilter(); 525 } 526 return doExpandValueSet(theExpansionOptions, theValueSetToExpand, ExpansionFilter.fromFilterString(filter)); 527 } 528 529 private ValueSet doExpandValueSet( 530 @Nullable ValueSetExpansionOptions theExpansionOptions, 531 ValueSet theValueSetToExpand, 532 ExpansionFilter theFilter) { 533 Set<String> addedCodes = new HashSet<>(); 534 ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSetToExpand, "ValueSet to expand can not be null"); 535 536 ValueSetExpansionOptions expansionOptions = provideExpansionOptions(theExpansionOptions); 537 int offset = expansionOptions.getOffset(); 538 int count = expansionOptions.getCount(); 539 540 ValueSetExpansionComponentWithConceptAccumulator accumulator = 541 new ValueSetExpansionComponentWithConceptAccumulator( 542 myContext, count, expansionOptions.isIncludeHierarchy()); 543 accumulator.setHardExpansionMaximumSize(myStorageSettings.getMaximumExpansionSize()); 544 accumulator.setSkipCountRemaining(offset); 545 accumulator.setIdentifier(UUID.randomUUID().toString()); 546 accumulator.setTimestamp(new Date()); 547 accumulator.setOffset(offset); 548 549 if (theExpansionOptions != null && isHibernateSearchEnabled()) { 550 accumulator.addParameter().setName("offset").setValue(new IntegerType(offset)); 551 accumulator.addParameter().setName("count").setValue(new IntegerType(count)); 552 } 553 554 myTxTemplate.executeWithoutResult(tx -> expandValueSetIntoAccumulator( 555 theValueSetToExpand, theExpansionOptions, accumulator, theFilter, true, addedCodes)); 556 557 if (accumulator.getTotalConcepts() != null) { 558 accumulator.setTotal(accumulator.getTotalConcepts()); 559 } 560 561 ValueSet valueSet = new ValueSet(); 562 valueSet.setUrl(theValueSetToExpand.getUrl()); 563 valueSet.setId(theValueSetToExpand.getId()); 564 valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE); 565 valueSet.setCompose(theValueSetToExpand.getCompose()); 566 valueSet.setExpansion(accumulator); 567 568 for (String next : accumulator.getMessages()) { 569 valueSet.getMeta() 570 .addExtension() 571 .setUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE) 572 .setValue(new StringType(next)); 573 } 574 575 if (expansionOptions.isIncludeHierarchy()) { 576 accumulator.applyHierarchy(); 577 } 578 579 return valueSet; 580 } 581 582 private void expandValueSetIntoAccumulator( 583 ValueSet theValueSetToExpand, 584 ValueSetExpansionOptions theExpansionOptions, 585 IValueSetConceptAccumulator theAccumulator, 586 ExpansionFilter theFilter, 587 boolean theAdd, 588 Set<String> theAddedCodes) { 589 Optional<TermValueSet> optionalTermValueSet; 590 if (theValueSetToExpand.hasUrl()) { 591 if (theValueSetToExpand.hasVersion()) { 592 optionalTermValueSet = myTermValueSetDao.findTermValueSetByUrlAndVersion( 593 theValueSetToExpand.getUrl(), theValueSetToExpand.getVersion()); 594 } else { 595 optionalTermValueSet = findCurrentTermValueSet(theValueSetToExpand.getUrl()); 596 } 597 } else { 598 optionalTermValueSet = Optional.empty(); 599 } 600 601 /* 602 * ValueSet doesn't exist in pre-expansion database, so perform in-memory expansion 603 */ 604 if (optionalTermValueSet.isEmpty()) { 605 ourLog.debug( 606 "ValueSet is not present in terminology tables. Will perform in-memory expansion without parameters. {}", 607 getValueSetInfo(theValueSetToExpand)); 608 String msg = myContext 609 .getLocalizer() 610 .getMessage( 611 TermReadSvcImpl.class, 612 "valueSetExpandedUsingInMemoryExpansion", 613 getValueSetInfo(theValueSetToExpand)); 614 theAccumulator.addMessage(msg); 615 doExpandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter, theAddedCodes); 616 return; 617 } 618 619 /* 620 * ValueSet exists in pre-expansion database, but pre-expansion is not yet complete so perform in-memory expansion 621 */ 622 TermValueSet termValueSet = optionalTermValueSet.get(); 623 if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) { 624 String msg = myContext 625 .getLocalizer() 626 .getMessage( 627 TermReadSvcImpl.class, 628 "valueSetNotYetExpanded", 629 getValueSetInfo(theValueSetToExpand), 630 termValueSet.getExpansionStatus().name(), 631 termValueSet.getExpansionStatus().getDescription()); 632 theAccumulator.addMessage(msg); 633 doExpandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter, theAddedCodes); 634 return; 635 } 636 637 /* 638 * ValueSet is pre-expanded in database so let's use that 639 */ 640 String expansionTimestamp = toHumanReadableExpansionTimestamp(termValueSet); 641 String msg = myContext 642 .getLocalizer() 643 .getMessage(TermReadSvcImpl.class, "valueSetExpandedUsingPreExpansion", expansionTimestamp); 644 theAccumulator.addMessage(msg); 645 expandConcepts( 646 theExpansionOptions, 647 theAccumulator, 648 termValueSet, 649 theFilter, 650 theAdd, 651 theAddedCodes, 652 myHibernatePropertiesProvider.isOracleDialect()); 653 } 654 655 @Nonnull 656 private String toHumanReadableExpansionTimestamp(TermValueSet termValueSet) { 657 String expansionTimestamp = "(unknown)"; 658 if (termValueSet.getExpansionTimestamp() != null) { 659 // Note: We used to append "123ms ago" to the timestamp, but we cache the 660 // results here, so it's just kind of weird to do that since the duration will 661 // be out of date when the entry comes back from cache 662 expansionTimestamp = new InstantType(termValueSet.getExpansionTimestamp()).getValueAsString(); 663 } 664 return expansionTimestamp; 665 } 666 667 private void expandConcepts( 668 ValueSetExpansionOptions theExpansionOptions, 669 IValueSetConceptAccumulator theAccumulator, 670 TermValueSet theTermValueSet, 671 ExpansionFilter theFilter, 672 boolean theAdd, 673 Set<String> theAddedCodes, 674 boolean theOracle) { 675 // NOTE: if you modifiy the logic here, look to `expandConceptsOracle` and see if your new code applies to its 676 // copy pasted sibling 677 Integer offset = theAccumulator.getSkipCountRemaining(); 678 offset = ObjectUtils.defaultIfNull(offset, 0); 679 offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue()); 680 681 Integer count = theAccumulator.getCapacityRemaining(); 682 count = defaultIfNull(count, myStorageSettings.getMaximumExpansionSize()); 683 684 int conceptsExpanded = 0; 685 int designationsExpanded = 0; 686 int toIndex = offset + count; 687 688 Collection<? extends ITermValueSetConceptView> conceptViews; 689 boolean wasFilteredResult = false; 690 String filterDisplayValue = null; 691 if (!theFilter.getFilters().isEmpty() 692 && JpaConstants.VALUESET_FILTER_DISPLAY.equals( 693 theFilter.getFilters().get(0).getProperty()) 694 && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) { 695 filterDisplayValue = 696 lowerCase(theFilter.getFilters().get(0).getValue().replace("%", "[%]")); 697 String displayValue = "%" + lowerCase(filterDisplayValue) + "%"; 698 if (theOracle) { 699 conceptViews = 700 myTermValueSetConceptViewOracleDao.findByTermValueSetId(theTermValueSet.getId(), displayValue); 701 } else { 702 conceptViews = myTermValueSetConceptViewDao.findByTermValueSetId(theTermValueSet.getId(), displayValue); 703 } 704 wasFilteredResult = true; 705 } else { 706 if (theOracle) { 707 conceptViews = myTermValueSetConceptViewOracleDao.findByTermValueSetId( 708 offset, toIndex, theTermValueSet.getId()); 709 } else { 710 conceptViews = 711 myTermValueSetConceptViewDao.findByTermValueSetId(offset, toIndex, theTermValueSet.getId()); 712 } 713 theAccumulator.consumeSkipCount(offset); 714 if (theAdd) { 715 theAccumulator.incrementOrDecrementTotalConcepts( 716 true, theTermValueSet.getTotalConcepts().intValue()); 717 } 718 } 719 720 if (conceptViews.isEmpty()) { 721 logConceptsExpanded("No concepts to expand. ", theTermValueSet, conceptsExpanded); 722 return; 723 } 724 725 Map<Long, FhirVersionIndependentConcept> pidToConcept = new LinkedHashMap<>(); 726 ArrayListMultimap<Long, TermConceptDesignation> pidToDesignations = ArrayListMultimap.create(); 727 Map<Long, Long> pidToSourcePid = new HashMap<>(); 728 Map<Long, String> pidToSourceDirectParentPids = new HashMap<>(); 729 730 for (ITermValueSetConceptView conceptView : conceptViews) { 731 732 String system = conceptView.getConceptSystemUrl(); 733 String code = conceptView.getConceptCode(); 734 String display = conceptView.getConceptDisplay(); 735 String systemVersion = conceptView.getConceptSystemVersion(); 736 737 // -- this is quick solution, may need to revisit 738 if (!applyFilter(display, filterDisplayValue)) { 739 continue; 740 } 741 742 Long conceptPid = conceptView.getConceptPid(); 743 if (!pidToConcept.containsKey(conceptPid)) { 744 FhirVersionIndependentConcept concept = 745 new FhirVersionIndependentConcept(system, code, display, systemVersion); 746 pidToConcept.put(conceptPid, concept); 747 } 748 749 // TODO: DM 2019-08-17 - Implement includeDesignations parameter for $expand operation to designations 750 // optional. 751 if (conceptView.getDesignationPid() != null) { 752 TermConceptDesignation designation = new TermConceptDesignation(); 753 754 if (isValueSetDisplayLanguageMatch(theExpansionOptions, conceptView.getDesignationLang())) { 755 designation.setUseSystem(conceptView.getDesignationUseSystem()); 756 designation.setUseCode(conceptView.getDesignationUseCode()); 757 designation.setUseDisplay(conceptView.getDesignationUseDisplay()); 758 designation.setValue(conceptView.getDesignationVal()); 759 designation.setLanguage(conceptView.getDesignationLang()); 760 pidToDesignations.put(conceptPid, designation); 761 } 762 763 if (++designationsExpanded % 250 == 0) { 764 logDesignationsExpanded( 765 "Expansion of designations in progress. ", theTermValueSet, designationsExpanded); 766 } 767 } 768 769 if (theAccumulator.isTrackingHierarchy()) { 770 pidToSourcePid.put(conceptPid, conceptView.getSourceConceptPid()); 771 pidToSourceDirectParentPids.put(conceptPid, conceptView.getSourceConceptDirectParentPids()); 772 } 773 774 if (++conceptsExpanded % 250 == 0) { 775 logConceptsExpanded("Expansion of concepts in progress. ", theTermValueSet, conceptsExpanded); 776 } 777 } 778 779 for (Long nextPid : pidToConcept.keySet()) { 780 FhirVersionIndependentConcept concept = pidToConcept.get(nextPid); 781 List<TermConceptDesignation> designations = pidToDesignations.get(nextPid); 782 String system = concept.getSystem(); 783 String code = concept.getCode(); 784 String display = concept.getDisplay(); 785 String systemVersion = concept.getSystemVersion(); 786 787 if (theAdd) { 788 if (theAccumulator.getCapacityRemaining() != null) { 789 if (theAccumulator.getCapacityRemaining() == 0) { 790 break; 791 } 792 } 793 794 Long sourceConceptPid = pidToSourcePid.get(nextPid); 795 String sourceConceptDirectParentPids = pidToSourceDirectParentPids.get(nextPid); 796 if (theAddedCodes.add(system + OUR_PIPE_CHARACTER + code)) { 797 theAccumulator.includeConceptWithDesignations( 798 system, 799 code, 800 display, 801 designations, 802 sourceConceptPid, 803 sourceConceptDirectParentPids, 804 systemVersion); 805 if (wasFilteredResult) { 806 theAccumulator.incrementOrDecrementTotalConcepts(true, 1); 807 } 808 } 809 } else { 810 if (theAddedCodes.remove(system + OUR_PIPE_CHARACTER + code)) { 811 theAccumulator.excludeConcept(system, code); 812 theAccumulator.incrementOrDecrementTotalConcepts(false, 1); 813 } 814 } 815 } 816 817 logDesignationsExpanded("Finished expanding designations. ", theTermValueSet, designationsExpanded); 818 logConceptsExpanded("Finished expanding concepts. ", theTermValueSet, conceptsExpanded); 819 } 820 821 private void logConceptsExpanded( 822 String theLogDescriptionPrefix, TermValueSet theTermValueSet, int theConceptsExpanded) { 823 if (theConceptsExpanded > 0) { 824 ourLog.debug( 825 "{}Have expanded {} concepts in ValueSet[{}]", 826 theLogDescriptionPrefix, 827 theConceptsExpanded, 828 theTermValueSet.getUrl()); 829 } 830 } 831 832 private void logDesignationsExpanded( 833 String theLogDescriptionPrefix, TermValueSet theTermValueSet, int theDesignationsExpanded) { 834 if (theDesignationsExpanded > 0) { 835 ourLog.debug( 836 "{}Have expanded {} designations in ValueSet[{}]", 837 theLogDescriptionPrefix, 838 theDesignationsExpanded, 839 theTermValueSet.getUrl()); 840 } 841 } 842 843 public boolean applyFilter(final String theDisplay, final String theFilterDisplay) { 844 845 // -- safety check only, no need to apply filter 846 if (theDisplay == null || theFilterDisplay == null) return true; 847 848 // -- sentence case 849 if (startsWithIgnoreCase(theDisplay, theFilterDisplay)) return true; 850 851 // -- token case 852 return startsWithByWordBoundaries(theDisplay, theFilterDisplay); 853 } 854 855 private boolean startsWithByWordBoundaries(String theDisplay, String theFilterDisplay) { 856 // return true only e.g. the input is 'Body height', theFilterDisplay is "he", or 'bo' 857 StringTokenizer tok = new StringTokenizer(theDisplay); 858 List<String> tokens = new ArrayList<>(); 859 while (tok.hasMoreTokens()) { 860 String token = tok.nextToken(); 861 if (startsWithIgnoreCase(token, theFilterDisplay)) return true; 862 tokens.add(token); 863 } 864 865 // Allow to search by the end of the phrase. E.g. "working proficiency" will match "Limited working 866 // proficiency" 867 for (int start = 0; start <= tokens.size() - 1; ++start) { 868 for (int end = start + 1; end <= tokens.size(); ++end) { 869 String sublist = String.join(" ", tokens.subList(start, end)); 870 if (startsWithIgnoreCase(sublist, theFilterDisplay)) return true; 871 } 872 } 873 return false; 874 } 875 876 @Override 877 public void expandValueSet( 878 ValueSetExpansionOptions theExpansionOptions, 879 ValueSet theValueSetToExpand, 880 IValueSetConceptAccumulator theValueSetCodeAccumulator) { 881 Set<String> addedCodes = new HashSet<>(); 882 doExpandValueSet( 883 theExpansionOptions, 884 theValueSetToExpand, 885 theValueSetCodeAccumulator, 886 ExpansionFilter.NO_FILTER, 887 addedCodes); 888 } 889 890 /** 891 * Note: Not transactional because specific calls within this method 892 * get executed in a transaction 893 */ 894 private void doExpandValueSet( 895 ValueSetExpansionOptions theExpansionOptions, 896 ValueSet theValueSetToExpand, 897 IValueSetConceptAccumulator theValueSetCodeAccumulator, 898 @Nonnull ExpansionFilter theExpansionFilter, 899 Set<String> theAddedCodes) { 900 901 StopWatch sw = new StopWatch(); 902 String valueSetInfo = getValueSetInfo(theValueSetToExpand); 903 ourLog.debug("Working with {}", valueSetInfo); 904 905 // Offset can't be combined with excludes 906 Integer skipCountRemaining = theValueSetCodeAccumulator.getSkipCountRemaining(); 907 if (skipCountRemaining != null && skipCountRemaining > 0) { 908 if (!theValueSetToExpand.getCompose().getExclude().isEmpty()) { 909 String msg = myContext 910 .getLocalizer() 911 .getMessage(TermReadSvcImpl.class, "valueSetNotYetExpanded_OffsetNotAllowed", valueSetInfo); 912 throw new InvalidRequestException(Msg.code(887) + msg); 913 } 914 } 915 916 // Handle includes 917 ourLog.debug("Handling includes"); 918 for (ValueSet.ConceptSetComponent include : 919 theValueSetToExpand.getCompose().getInclude()) { 920 myTxTemplate.executeWithoutResult(tx -> expandValueSetHandleIncludeOrExclude( 921 theExpansionOptions, theValueSetCodeAccumulator, theAddedCodes, include, true, theExpansionFilter)); 922 } 923 924 // Handle excludes 925 ourLog.debug("Handling excludes"); 926 for (ValueSet.ConceptSetComponent exclude : 927 theValueSetToExpand.getCompose().getExclude()) { 928 myTxTemplate.executeWithoutResult(tx -> expandValueSetHandleIncludeOrExclude( 929 theExpansionOptions, 930 theValueSetCodeAccumulator, 931 theAddedCodes, 932 exclude, 933 false, 934 ExpansionFilter.NO_FILTER)); 935 } 936 937 if (theValueSetCodeAccumulator instanceof ValueSetConceptAccumulator) { 938 myTxTemplate.execute( 939 t -> ((ValueSetConceptAccumulator) theValueSetCodeAccumulator).removeGapsFromConceptOrder()); 940 } 941 942 ourLog.debug("Done working with {} in {}ms", valueSetInfo, sw.getMillis()); 943 } 944 945 private String getValueSetInfo(ValueSet theValueSet) { 946 StringBuilder sb = new StringBuilder(); 947 boolean isIdentified = false; 948 if (theValueSet.hasUrl()) { 949 isIdentified = true; 950 sb.append("ValueSet.url[").append(theValueSet.getUrl()).append("]"); 951 } else if (theValueSet.hasId()) { 952 isIdentified = true; 953 sb.append("ValueSet.id[").append(theValueSet.getId()).append("]"); 954 } 955 956 if (!isIdentified) { 957 sb.append("Unidentified ValueSet"); 958 } 959 960 return sb.toString(); 961 } 962 963 /** 964 * Returns true if there are potentially more results to process. 965 */ 966 private void expandValueSetHandleIncludeOrExclude( 967 @Nullable ValueSetExpansionOptions theExpansionOptions, 968 IValueSetConceptAccumulator theValueSetCodeAccumulator, 969 Set<String> theAddedCodes, 970 ValueSet.ConceptSetComponent theIncludeOrExclude, 971 boolean theAdd, 972 @Nonnull ExpansionFilter theExpansionFilter) { 973 974 String system = theIncludeOrExclude.getSystem(); 975 boolean hasSystem = isNotBlank(system); 976 boolean hasValueSet = !theIncludeOrExclude.getValueSet().isEmpty(); 977 978 if (hasSystem) { 979 980 if (theExpansionFilter.hasCode() 981 && theExpansionFilter.getSystem() != null 982 && !system.equals(theExpansionFilter.getSystem())) { 983 return; 984 } 985 986 ourLog.debug("Starting {} expansion around CodeSystem: {}", (theAdd ? "inclusion" : "exclusion"), system); 987 988 Optional<TermCodeSystemVersion> termCodeSystemVersion = 989 optionalFindTermCodeSystemVersion(theIncludeOrExclude); 990 if (termCodeSystemVersion.isPresent()) { 991 992 expandValueSetHandleIncludeOrExcludeUsingDatabase( 993 theExpansionOptions, 994 theValueSetCodeAccumulator, 995 theAddedCodes, 996 theIncludeOrExclude, 997 theAdd, 998 theExpansionFilter, 999 system, 1000 termCodeSystemVersion.get()); 1001 1002 } else { 1003 1004 if (!theIncludeOrExclude.getConcept().isEmpty() && theExpansionFilter.hasCode()) { 1005 if (defaultString(theIncludeOrExclude.getSystem()).equals(theExpansionFilter.getSystem())) { 1006 if (theIncludeOrExclude.getConcept().stream() 1007 .noneMatch(t -> t.getCode().equals(theExpansionFilter.getCode()))) { 1008 return; 1009 } 1010 } 1011 } 1012 1013 Consumer<FhirVersionIndependentConcept> consumer = c -> addOrRemoveCode( 1014 theValueSetCodeAccumulator, 1015 theAddedCodes, 1016 theAdd, 1017 system, 1018 c.getCode(), 1019 c.getDisplay(), 1020 c.getSystemVersion()); 1021 1022 try { 1023 ConversionContext40_50.INSTANCE.init( 1024 new VersionConvertor_40_50(new BaseAdvisor_40_50()), "ValueSet"); 1025 org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent includeOrExclude = 1026 ValueSet40_50.convertConceptSetComponent(theIncludeOrExclude); 1027 myInMemoryTerminologyServerValidationSupport.expandValueSetIncludeOrExclude( 1028 new ValidationSupportContext(provideValidationSupport()), consumer, includeOrExclude); 1029 } catch (InMemoryTerminologyServerValidationSupport.ExpansionCouldNotBeCompletedInternallyException e) { 1030 if (theExpansionOptions != null 1031 && !theExpansionOptions.isFailOnMissingCodeSystem() 1032 // Code system is unknown, therefore NOT_FOUND 1033 && e.getCodeValidationIssue() 1034 .hasIssueDetailCode(CodeValidationIssueCoding.NOT_FOUND.getCode())) { 1035 return; 1036 } 1037 throw new InternalErrorException(Msg.code(888) + e); 1038 } finally { 1039 ConversionContext40_50.INSTANCE.close("ValueSet"); 1040 } 1041 } 1042 1043 } else if (hasValueSet) { 1044 1045 for (CanonicalType nextValueSet : theIncludeOrExclude.getValueSet()) { 1046 String valueSetUrl = nextValueSet.getValueAsString(); 1047 ourLog.debug( 1048 "Starting {} expansion around ValueSet: {}", (theAdd ? "inclusion" : "exclusion"), valueSetUrl); 1049 1050 ExpansionFilter subExpansionFilter = new ExpansionFilter( 1051 theExpansionFilter, 1052 theIncludeOrExclude.getFilter(), 1053 theValueSetCodeAccumulator.getCapacityRemaining()); 1054 1055 // TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not 1056 // be found in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the 1057 // expansion may time-out. 1058 1059 ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(valueSetUrl); 1060 if (valueSet == null) { 1061 throw new ResourceNotFoundException( 1062 Msg.code(889) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(valueSetUrl)); 1063 } 1064 1065 expandValueSetIntoAccumulator( 1066 valueSet, 1067 theExpansionOptions, 1068 theValueSetCodeAccumulator, 1069 subExpansionFilter, 1070 theAdd, 1071 theAddedCodes); 1072 } 1073 1074 } else { 1075 throw new InvalidRequestException(Msg.code(890) + "ValueSet contains " + (theAdd ? "include" : "exclude") 1076 + " criteria with no system defined"); 1077 } 1078 } 1079 1080 private Optional<TermCodeSystemVersion> optionalFindTermCodeSystemVersion( 1081 ValueSet.ConceptSetComponent theIncludeOrExclude) { 1082 if (isEmpty(theIncludeOrExclude.getVersion())) { 1083 return Optional.ofNullable(myCodeSystemDao.findByCodeSystemUri(theIncludeOrExclude.getSystem())) 1084 .map(TermCodeSystem::getCurrentVersion); 1085 } else { 1086 return Optional.ofNullable(myCodeSystemVersionDao.findByCodeSystemUriAndVersion( 1087 theIncludeOrExclude.getSystem(), theIncludeOrExclude.getVersion())); 1088 } 1089 } 1090 1091 private boolean isHibernateSearchEnabled() { 1092 return myFulltextSearchSvc != null && !ourForceDisableHibernateSearchForUnitTest; 1093 } 1094 1095 private void expandValueSetHandleIncludeOrExcludeUsingDatabase( 1096 ValueSetExpansionOptions theExpansionOptions, 1097 IValueSetConceptAccumulator theValueSetCodeAccumulator, 1098 Set<String> theAddedCodes, 1099 ValueSet.ConceptSetComponent theIncludeOrExclude, 1100 boolean theAdd, 1101 @Nonnull ExpansionFilter theExpansionFilter, 1102 String theSystem, 1103 TermCodeSystemVersion theTermCodeSystemVersion) { 1104 1105 StopWatch fullOperationSw = new StopWatch(); 1106 String includeOrExcludeVersion = theIncludeOrExclude.getVersion(); 1107 1108 /* 1109 * If FullText searching is not enabled, we can handle only basic expansions 1110 * since we're going to do it without the database. 1111 */ 1112 if (!isHibernateSearchEnabled()) { 1113 expandWithoutHibernateSearch( 1114 theValueSetCodeAccumulator, 1115 theTermCodeSystemVersion, 1116 theAddedCodes, 1117 theIncludeOrExclude, 1118 theSystem, 1119 theAdd); 1120 return; 1121 } 1122 1123 /* 1124 * Ok, let's use hibernate search to build the expansion 1125 */ 1126 1127 int count = 0; 1128 1129 Optional<Integer> chunkSizeOpt = getScrollChunkSize(theAdd, theValueSetCodeAccumulator); 1130 if (chunkSizeOpt.isEmpty()) { 1131 return; 1132 } 1133 int chunkSize = chunkSizeOpt.get(); 1134 1135 /* 1136 * Turn the filter into one or more Hibernate Search queries. Ideally we want it 1137 * to be handled by a single query, but Lucene/ES don't like it when we exceed 1138 * 1024 different terms in a single query. So if we have that many terms (which 1139 * can happen if a ValueSet has a lot of explicitly enumerated codes that it's 1140 * including) we split this into multiple searches. The method below builds these 1141 * searches lazily, returning a Supplier that creates and executes the search 1142 * when it's actually time to. 1143 */ 1144 SearchProperties searchProps = buildSearchScrolls( 1145 theTermCodeSystemVersion, 1146 theExpansionFilter, 1147 theSystem, 1148 theIncludeOrExclude, 1149 chunkSize, 1150 includeOrExcludeVersion); 1151 int accumulatedBatchesSoFar = 0; 1152 for (var next : searchProps.getSearchScroll()) { 1153 try (SearchScroll<EntityReference> scroll = next.get()) { 1154 ourLog.debug( 1155 "Beginning batch expansion for {} with max results per batch: {}", 1156 (theAdd ? "inclusion" : "exclusion"), 1157 chunkSize); 1158 for (SearchScrollResult<EntityReference> chunk = scroll.next(); 1159 chunk.hasHits(); 1160 chunk = scroll.next()) { 1161 int countForBatch = 0; 1162 1163 List<TermConcept.TermConceptPk> pids = chunk.hits().stream() 1164 .map(t -> (TermConcept.TermConceptPk) t.id()) 1165 .collect(Collectors.toList()); 1166 1167 /* 1168 * This is a (hopefully) temporary hack - we are pulling concepts 1169 * out of ElasticSearch here and ES concepts aren't yet partition 1170 * aware. So we'll assume that they meant the default partition. This 1171 * will always work for now since terminology stuff is non-partitionable 1172 * but that could change in the future. 1173 */ 1174 for (var pid : pids) { 1175 if (pid.getPartitionIdValue() == null) { 1176 pid.setPartitionIdValue(myPartitionSettings.getDefaultPartitionId()); 1177 } 1178 } 1179 1180 List<TermConcept> termConcepts = myTermConceptDao.fetchConceptsAndDesignationsByPid(pids); 1181 1182 // If the include section had multiple codes, return the codes in the same order 1183 termConcepts = sortTermConcepts(searchProps, termConcepts); 1184 1185 // int firstResult = theQueryIndex * maxResultsPerBatch; 1186 // TODO GGG HS we lose the ability to check the 1187 // index of the first result, so just best-guessing it here. 1188 int delta = 0; 1189 for (TermConcept concept : termConcepts) { 1190 count++; 1191 countForBatch++; 1192 String conceptDisplayValue = concept.getDisplay(); 1193 if (theAdd && searchProps.hasIncludeOrExcludeCodes()) { 1194 ValueSet.ConceptReferenceComponent theIncludeConcept = 1195 getMatchedConceptIncludedInValueSet(theIncludeOrExclude, concept); 1196 if (theIncludeConcept != null && isNotBlank(theIncludeConcept.getDisplay())) { 1197 conceptDisplayValue = theIncludeConcept.getDisplay(); 1198 } 1199 } 1200 boolean added = addCodeIfNotAlreadyAdded( 1201 theExpansionOptions, 1202 theValueSetCodeAccumulator, 1203 theAddedCodes, 1204 concept, 1205 conceptDisplayValue, 1206 theAdd, 1207 includeOrExcludeVersion); 1208 if (added) { 1209 delta++; 1210 } 1211 } 1212 1213 ourLog.debug( 1214 "Batch expansion scroll for {} with offset {} produced {} results in {}ms", 1215 (theAdd ? "inclusion" : "exclusion"), 1216 accumulatedBatchesSoFar, 1217 chunk.hits().size(), 1218 chunk.took().toMillis()); 1219 1220 theValueSetCodeAccumulator.incrementOrDecrementTotalConcepts(theAdd, delta); 1221 accumulatedBatchesSoFar += countForBatch; 1222 1223 // keep session bounded 1224 myEntityManager.flush(); 1225 myEntityManager.clear(); 1226 } 1227 1228 ourLog.debug( 1229 "Expansion for {} produced {} results in {}ms", 1230 (theAdd ? "inclusion" : "exclusion"), 1231 count, 1232 fullOperationSw.getMillis()); 1233 } 1234 } 1235 } 1236 1237 private List<TermConcept> sortTermConcepts(SearchProperties searchProps, List<TermConcept> termConcepts) { 1238 List<String> codes = searchProps.getIncludeOrExcludeCodes(); 1239 if (codes.size() > 1) { 1240 termConcepts = new ArrayList<>(termConcepts); 1241 Map<String, Integer> codeToIndex = new HashMap<>(codes.size()); 1242 for (int i = 0; i < codes.size(); i++) { 1243 codeToIndex.put(codes.get(i), i); 1244 } 1245 termConcepts.sort(((o1, o2) -> { 1246 Integer idx1 = codeToIndex.get(o1.getCode()); 1247 Integer idx2 = codeToIndex.get(o2.getCode()); 1248 return Comparators.nullsHigh().compare(idx1, idx2); 1249 })); 1250 } 1251 return termConcepts; 1252 } 1253 1254 private Optional<Integer> getScrollChunkSize( 1255 boolean theAdd, IValueSetConceptAccumulator theValueSetCodeAccumulator) { 1256 int maxResultsPerBatch = SearchBuilder.getMaximumPageSize(); 1257 1258 /* 1259 * If the accumulator is bounded, we may reduce the size of the query to 1260 * Lucene in order to be more efficient. 1261 */ 1262 if (theAdd) { 1263 Integer accumulatorCapacityRemaining = theValueSetCodeAccumulator.getCapacityRemaining(); 1264 if (accumulatorCapacityRemaining != null) { 1265 maxResultsPerBatch = Math.min(maxResultsPerBatch, accumulatorCapacityRemaining + 1); 1266 } 1267 } 1268 return maxResultsPerBatch > 0 ? Optional.of(maxResultsPerBatch) : Optional.empty(); 1269 } 1270 1271 private SearchProperties buildSearchScrolls( 1272 TermCodeSystemVersion theTermCodeSystemVersion, 1273 ExpansionFilter theExpansionFilter, 1274 String theSystem, 1275 ValueSet.ConceptSetComponent theIncludeOrExclude, 1276 Integer theScrollChunkSize, 1277 String theIncludeOrExcludeVersion) { 1278 SearchSession searchSession = Search.session(myEntityManager); 1279 // Manually building a predicate since we need to throw it around. 1280 SearchPredicateFactory predicate = 1281 searchSession.scope(TermConcept.class).predicate(); 1282 1283 List<String> allCodes = theIncludeOrExclude.getConcept().stream() 1284 .filter(Objects::nonNull) 1285 .map(ValueSet.ConceptReferenceComponent::getCode) 1286 .filter(StringUtils::isNotBlank) 1287 .collect(Collectors.toList()); 1288 SearchProperties returnProps = new SearchProperties(); 1289 returnProps.setIncludeOrExcludeCodes(allCodes); 1290 1291 /* 1292 * Lucene/ES can't typically handle more than 1024 clauses per search, so if 1293 * we have more than that number (e.g. because of a ValueSet that explicitly 1294 * includes thousands of codes), we break this up into multiple searches. 1295 */ 1296 List<List<String>> partitionedCodes = ListUtils.partition(allCodes, IndexSearcher.getMaxClauseCount() - 10); 1297 if (partitionedCodes.isEmpty()) { 1298 partitionedCodes = List.of(List.of()); 1299 } 1300 1301 for (List<String> nextCodePartition : partitionedCodes) { 1302 Supplier<SearchScroll<EntityReference>> nextScroll = () -> { 1303 // Build the top-level expansion on filters. 1304 PredicateFinalStep step = predicate.bool(b -> { 1305 b.must(predicate 1306 .match() 1307 .field("myCodeSystemVersionPid") 1308 .matching(theTermCodeSystemVersion.getPid())); 1309 1310 if (theExpansionFilter.hasCode()) { 1311 b.must(predicate.match().field("myCode").matching(theExpansionFilter.getCode())); 1312 } 1313 1314 String codeSystemUrlAndVersion = 1315 buildCodeSystemUrlAndVersion(theSystem, theIncludeOrExcludeVersion); 1316 for (ValueSet.ConceptSetFilterComponent nextFilter : theIncludeOrExclude.getFilter()) { 1317 handleFilter(codeSystemUrlAndVersion, predicate, b, nextFilter); 1318 } 1319 for (ValueSet.ConceptSetFilterComponent nextFilter : theExpansionFilter.getFilters()) { 1320 handleFilter(codeSystemUrlAndVersion, predicate, b, nextFilter); 1321 } 1322 }); 1323 1324 // Add a selector on any explicitly enumerated codes in the VS component 1325 final PredicateFinalStep finishedQuery; 1326 if (nextCodePartition.isEmpty()) { 1327 finishedQuery = step; 1328 } else { 1329 PredicateFinalStep expansionStep = buildExpansionPredicate(nextCodePartition, predicate); 1330 finishedQuery = predicate.bool().must(step).must(expansionStep); 1331 } 1332 1333 SearchQuery<EntityReference> termConceptsQuery = searchSession 1334 .search(TermConcept.class) 1335 .selectEntityReference() 1336 .where(f -> finishedQuery) 1337 .toQuery(); 1338 1339 return termConceptsQuery.scroll(theScrollChunkSize); 1340 }; 1341 1342 returnProps.addSearchScroll(nextScroll); 1343 } 1344 1345 return returnProps; 1346 } 1347 1348 private ValueSet.ConceptReferenceComponent getMatchedConceptIncludedInValueSet( 1349 ValueSet.ConceptSetComponent theIncludeOrExclude, TermConcept concept) { 1350 return theIncludeOrExclude.getConcept().stream() 1351 .filter(includedConcept -> includedConcept.getCode().equalsIgnoreCase(concept.getCode())) 1352 .findFirst() 1353 .orElse(null); 1354 } 1355 1356 /** 1357 * Helper method which builds a predicate for the expansion 1358 */ 1359 private PredicateFinalStep buildExpansionPredicate(List<String> theCodes, SearchPredicateFactory thePredicate) { 1360 assert !theCodes.isEmpty(); 1361 return thePredicate.simpleQueryString().field("myCode").matching(String.join(" | ", theCodes)); 1362 } 1363 1364 private String buildCodeSystemUrlAndVersion(String theSystem, String theIncludeOrExcludeVersion) { 1365 String codeSystemUrlAndVersion; 1366 if (theIncludeOrExcludeVersion != null) { 1367 codeSystemUrlAndVersion = theSystem + OUR_PIPE_CHARACTER + theIncludeOrExcludeVersion; 1368 } else { 1369 codeSystemUrlAndVersion = theSystem; 1370 } 1371 return codeSystemUrlAndVersion; 1372 } 1373 1374 private @Nonnull ValueSetExpansionOptions provideExpansionOptions( 1375 @Nullable ValueSetExpansionOptions theExpansionOptions) { 1376 return Objects.requireNonNullElse(theExpansionOptions, DEFAULT_EXPANSION_OPTIONS); 1377 } 1378 1379 private void addOrRemoveCode( 1380 IValueSetConceptAccumulator theValueSetCodeAccumulator, 1381 Set<String> theAddedCodes, 1382 boolean theAdd, 1383 String theSystem, 1384 String theCode, 1385 String theDisplay, 1386 String theSystemVersion) { 1387 if (theAdd && theAddedCodes.add(theSystem + OUR_PIPE_CHARACTER + theCode)) { 1388 theValueSetCodeAccumulator.includeConcept(theSystem, theCode, theDisplay, null, null, theSystemVersion); 1389 } 1390 if (!theAdd && theAddedCodes.remove(theSystem + OUR_PIPE_CHARACTER + theCode)) { 1391 theValueSetCodeAccumulator.excludeConcept(theSystem, theCode); 1392 } 1393 } 1394 1395 private void handleFilter( 1396 String theCodeSystemIdentifier, 1397 SearchPredicateFactory theF, 1398 BooleanPredicateClausesStep<?> theB, 1399 ValueSet.ConceptSetFilterComponent theFilter) { 1400 if (isBlank(theFilter.getValue()) && theFilter.getOp() == null && isBlank(theFilter.getProperty())) { 1401 return; 1402 } 1403 1404 // if filter type is EXISTS, there's no reason to worry about the value (we won't set it anyways) 1405 if ((isBlank(theFilter.getValue()) && theFilter.getOp() != ValueSet.FilterOperator.EXISTS) 1406 || theFilter.getOp() == null 1407 || isBlank(theFilter.getProperty())) { 1408 throw new InvalidRequestException( 1409 Msg.code(891) + "Invalid filter, must have fields populated: property op value"); 1410 } 1411 1412 switch (theFilter.getProperty()) { 1413 case "display:exact": 1414 case "display": 1415 handleFilterDisplay(theF, theB, theFilter); 1416 break; 1417 case "concept": 1418 case "code": 1419 handleFilterConceptAndCode(theCodeSystemIdentifier, theF, theB, theFilter); 1420 break; 1421 case "parent": 1422 case "child": 1423 isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty()); 1424 handleFilterLoincParentChild(theF, theB, theFilter); 1425 break; 1426 case "ancestor": 1427 isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty()); 1428 handleFilterLoincAncestor(theCodeSystemIdentifier, theF, theB, theFilter); 1429 break; 1430 case "descendant": 1431 isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty()); 1432 handleFilterLoincDescendant(theCodeSystemIdentifier, theF, theB, theFilter); 1433 break; 1434 case "copyright": 1435 isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty()); 1436 handleFilterLoincCopyright(theF, theB, theFilter); 1437 break; 1438 default: 1439 if (theFilter.getOp() == ValueSet.FilterOperator.REGEX) { 1440 handleFilterRegex(theF, theB, theFilter); 1441 } else { 1442 handleFilterPropertyDefault(theF, theB, theFilter); 1443 } 1444 break; 1445 } 1446 } 1447 1448 private void handleFilterPropertyDefault( 1449 SearchPredicateFactory theF, 1450 BooleanPredicateClausesStep<?> theB, 1451 ValueSet.ConceptSetFilterComponent theFilter) { 1452 1453 String value = theFilter.getValue(); 1454 if (theFilter.getOp() == ValueSet.FilterOperator.EXISTS) { 1455 // EXISTS has no value and is thus handled differently 1456 Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty()); 1457 theB.must(theF.exists().field(term.field())); 1458 } else { 1459 Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty(), value); 1460 switch (theFilter.getOp()) { 1461 case EQUAL: 1462 theB.must(theF.match().field(term.field()).matching(term.text())); 1463 break; 1464 case IN: 1465 case NOTIN: 1466 boolean isNotFilter = theFilter.getOp() == ValueSet.FilterOperator.NOTIN; 1467 // IN and NOTIN expect comma separated lists 1468 String[] values = term.text().split(","); 1469 Set<String> valueSet = new HashSet<>(Arrays.asList(values)); 1470 if (isNotFilter) { 1471 theB.filter(theF.not(theF.terms().field(term.field()).matchingAny(valueSet))); 1472 } else { 1473 theB.filter(theF.terms().field(term.field()).matchingAny(valueSet)); 1474 } 1475 break; 1476 case REGEX: 1477 case EXISTS: 1478 case NULL: 1479 case ISA: 1480 case ISNOTA: 1481 case DESCENDENTOF: 1482 case GENERALIZES: 1483 default: 1484 /* 1485 * We do not need to handle REGEX, because that's handled in parent 1486 * We also don't handle EXISTS because that's a separate area (with different term). 1487 * We add a match-none filter because otherwise it matches everything (not desired). 1488 */ 1489 ourLog.error( 1490 "Unsupported property filter {}. This may affect expansion, but will not cause errors.", 1491 theFilter.getOp().getDisplay()); 1492 theB.must(theF.matchNone()); 1493 break; 1494 } 1495 } 1496 } 1497 1498 private void handleFilterRegex( 1499 SearchPredicateFactory theF, 1500 BooleanPredicateClausesStep<?> theB, 1501 ValueSet.ConceptSetFilterComponent theFilter) { 1502 /* 1503 * We treat the regex filter as a match on the regex 1504 * anywhere in the property string. The spec does not 1505 * say whether this is the right behaviour or not, but 1506 * there are examples that seem to suggest that it is. 1507 */ 1508 String value = theFilter.getValue(); 1509 if (value.endsWith("$")) { 1510 value = value.substring(0, value.length() - 1); 1511 } else if (!value.endsWith(".*")) { 1512 value = value + ".*"; 1513 } 1514 if (!value.startsWith("^") && !value.startsWith(".*")) { 1515 value = ".*" + value; 1516 } else if (value.startsWith("^")) { 1517 value = value.substring(1); 1518 } 1519 1520 theB.must(theF.regexp() 1521 .field(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty()) 1522 .matching(value)); 1523 } 1524 1525 private void handleFilterLoincCopyright( 1526 SearchPredicateFactory theF, 1527 BooleanPredicateClausesStep<?> theB, 1528 ValueSet.ConceptSetFilterComponent theFilter) { 1529 1530 if (theFilter.getOp() == ValueSet.FilterOperator.EQUAL) { 1531 1532 String copyrightFilterValue = defaultString(theFilter.getValue()).toLowerCase(); 1533 switch (copyrightFilterValue) { 1534 case "3rdparty": 1535 logFilteringValueOnProperty(theFilter.getValue(), theFilter.getProperty()); 1536 addFilterLoincCopyright3rdParty(theF, theB); 1537 break; 1538 case "loinc": 1539 logFilteringValueOnProperty(theFilter.getValue(), theFilter.getProperty()); 1540 addFilterLoincCopyrightLoinc(theF, theB); 1541 break; 1542 default: 1543 throwInvalidRequestForValueOnProperty(theFilter.getValue(), theFilter.getProperty()); 1544 } 1545 1546 } else { 1547 throwInvalidRequestForOpOnProperty(theFilter.getOp(), theFilter.getProperty()); 1548 } 1549 } 1550 1551 private void addFilterLoincCopyrightLoinc(SearchPredicateFactory theF, BooleanPredicateClausesStep<?> theB) { 1552 theB.mustNot(theF.exists().field(CONCEPT_PROPERTY_PREFIX_NAME + "EXTERNAL_COPYRIGHT_NOTICE")); 1553 } 1554 1555 private void addFilterLoincCopyright3rdParty(SearchPredicateFactory theF, BooleanPredicateClausesStep<?> theB) { 1556 theB.must(theF.exists().field(CONCEPT_PROPERTY_PREFIX_NAME + "EXTERNAL_COPYRIGHT_NOTICE")); 1557 } 1558 1559 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 1560 private void handleFilterLoincAncestor( 1561 String theSystem, 1562 SearchPredicateFactory f, 1563 BooleanPredicateClausesStep<?> b, 1564 ValueSet.ConceptSetFilterComponent theFilter) { 1565 switch (theFilter.getOp()) { 1566 case EQUAL: 1567 addLoincFilterAncestorEqual(theSystem, f, b, theFilter); 1568 break; 1569 case IN: 1570 addLoincFilterAncestorIn(theSystem, f, b, theFilter); 1571 break; 1572 default: 1573 throw new InvalidRequestException(Msg.code(892) + "Don't know how to handle op=" + theFilter.getOp() 1574 + " on property " + theFilter.getProperty()); 1575 } 1576 } 1577 1578 private void addLoincFilterAncestorEqual( 1579 String theSystem, 1580 SearchPredicateFactory f, 1581 BooleanPredicateClausesStep<?> b, 1582 ValueSet.ConceptSetFilterComponent theFilter) { 1583 addLoincFilterAncestorEqual(theSystem, f, b, theFilter.getProperty(), theFilter.getValue()); 1584 } 1585 1586 private void addLoincFilterAncestorEqual( 1587 String theSystem, 1588 SearchPredicateFactory f, 1589 BooleanPredicateClausesStep<?> b, 1590 String theProperty, 1591 String theValue) { 1592 List<Term> terms = getAncestorTerms(theSystem, theProperty, theValue); 1593 b.must(f.bool(innerB -> terms.forEach( 1594 term -> innerB.should(f.match().field(term.field()).matching(term.text()))))); 1595 } 1596 1597 private void addLoincFilterAncestorIn( 1598 String theSystem, 1599 SearchPredicateFactory f, 1600 BooleanPredicateClausesStep<?> b, 1601 ValueSet.ConceptSetFilterComponent theFilter) { 1602 String[] values = theFilter.getValue().split(","); 1603 List<Term> terms = new ArrayList<>(); 1604 for (String value : values) { 1605 terms.addAll(getAncestorTerms(theSystem, theFilter.getProperty(), value)); 1606 } 1607 b.must(f.bool(innerB -> terms.forEach( 1608 term -> innerB.should(f.match().field(term.field()).matching(term.text()))))); 1609 } 1610 1611 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 1612 private void handleFilterLoincParentChild( 1613 SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) { 1614 switch (theFilter.getOp()) { 1615 case EQUAL: 1616 addLoincFilterParentChildEqual(f, b, theFilter.getProperty(), theFilter.getValue()); 1617 break; 1618 case IN: 1619 addLoincFilterParentChildIn(f, b, theFilter); 1620 break; 1621 default: 1622 throw new InvalidRequestException(Msg.code(893) + "Don't know how to handle op=" + theFilter.getOp() 1623 + " on property " + theFilter.getProperty()); 1624 } 1625 } 1626 1627 private void addLoincFilterParentChildIn( 1628 SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) { 1629 String[] values = theFilter.getValue().split(","); 1630 List<Term> terms = new ArrayList<>(); 1631 for (String value : values) { 1632 logFilteringValueOnProperty(value, theFilter.getProperty()); 1633 terms.add(getPropertyTerm(theFilter.getProperty(), value)); 1634 } 1635 1636 b.must(f.bool(innerB -> terms.forEach( 1637 term -> innerB.should(f.match().field(term.field()).matching(term.text()))))); 1638 } 1639 1640 private void addLoincFilterParentChildEqual( 1641 SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, String theProperty, String theValue) { 1642 logFilteringValueOnProperty(theValue, theProperty); 1643 b.must(f.match().field(CONCEPT_PROPERTY_PREFIX_NAME + theProperty).matching(theValue)); 1644 } 1645 1646 private void handleFilterConceptAndCode( 1647 String theSystem, 1648 SearchPredicateFactory f, 1649 BooleanPredicateClausesStep<?> b, 1650 ValueSet.ConceptSetFilterComponent theFilter) { 1651 TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, theFilter); 1652 1653 if (theFilter.getOp() == ValueSet.FilterOperator.ISA) { 1654 ourLog.debug( 1655 " * Filtering on specific code and codes with a parent of {}/{}/{}", 1656 code.getId(), 1657 code.getCode(), 1658 code.getDisplay()); 1659 1660 b.must(f.bool() 1661 .should(f.match().field("myParentPids").matching("" + code.getId())) 1662 .should(f.match().field("myId").matching(code.getPid()))); 1663 } else if (theFilter.getOp() == ValueSet.FilterOperator.DESCENDENTOF) { 1664 ourLog.debug( 1665 " * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay()); 1666 1667 b.must(f.match().field("myParentPids").matching("" + code.getId())); 1668 } else { 1669 throwInvalidFilter(theFilter, ""); 1670 } 1671 } 1672 1673 @Nonnull 1674 private TermConcept findCodeForFilterCriteriaCodeOrConcept( 1675 String theSystem, ValueSet.ConceptSetFilterComponent theFilter) { 1676 return findCode(theSystem, theFilter.getValue()) 1677 .orElseThrow(() -> 1678 new InvalidRequestException(Msg.code(2071) + "Invalid filter criteria - code does not exist: {" 1679 + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theFilter.getValue())); 1680 } 1681 1682 private void throwInvalidFilter(ValueSet.ConceptSetFilterComponent theFilter, String theErrorSuffix) { 1683 throw new InvalidRequestException(Msg.code(894) + "Don't know how to handle op=" + theFilter.getOp() 1684 + " on property " + theFilter.getProperty() + theErrorSuffix); 1685 } 1686 1687 private void isCodeSystemLoincOrThrowInvalidRequestException(String theSystemIdentifier, String theProperty) { 1688 String systemUrl = getUrlFromIdentifier(theSystemIdentifier); 1689 if (!isCodeSystemLoinc(systemUrl)) { 1690 throw new InvalidRequestException(Msg.code(895) + "Invalid filter, property " + theProperty 1691 + " is LOINC-specific and cannot be used with system: " + systemUrl); 1692 } 1693 } 1694 1695 private boolean isCodeSystemLoinc(String theSystem) { 1696 return LOINC_URI.equals(theSystem); 1697 } 1698 1699 private void handleFilterDisplay( 1700 SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) { 1701 if (theFilter.getProperty().equals("display:exact") && theFilter.getOp() == ValueSet.FilterOperator.EQUAL) { 1702 addDisplayFilterExact(f, b, theFilter); 1703 } else if (theFilter.getProperty().equals("display") && theFilter.getOp() == ValueSet.FilterOperator.EQUAL) { 1704 if (theFilter.getValue().trim().contains(" ")) { 1705 addDisplayFilterExact(f, b, theFilter); 1706 } else { 1707 addDisplayFilterInexact(f, b, theFilter); 1708 } 1709 } 1710 } 1711 1712 private void addDisplayFilterExact( 1713 SearchPredicateFactory f, 1714 BooleanPredicateClausesStep<?> bool, 1715 ValueSet.ConceptSetFilterComponent nextFilter) { 1716 bool.must(f.phrase().field("myDisplay").matching(nextFilter.getValue())); 1717 } 1718 1719 private void addDisplayFilterInexact( 1720 SearchPredicateFactory f, 1721 BooleanPredicateClausesStep<?> bool, 1722 ValueSet.ConceptSetFilterComponent nextFilter) { 1723 bool.must(f.phrase() 1724 .field("myDisplay") 1725 .boost(4.0f) 1726 .field("myDisplayWordEdgeNGram") 1727 .boost(1.0f) 1728 .field("myDisplayEdgeNGram") 1729 .boost(1.0f) 1730 .matching(nextFilter.getValue().toLowerCase()) 1731 .slop(2)); 1732 } 1733 1734 private Term getPropertyTerm(String theProperty, String theValue) { 1735 return new Term(CONCEPT_PROPERTY_PREFIX_NAME + theProperty, theValue); 1736 } 1737 1738 private List<Term> getAncestorTerms(String theSystem, String theProperty, String theValue) { 1739 List<Term> retVal = new ArrayList<>(); 1740 1741 TermConcept code = findCode(theSystem, theValue) 1742 .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {" 1743 + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue)); 1744 1745 retVal.add(new Term("myParentPids", "" + code.getId())); 1746 logFilteringValueOnProperty(theValue, theProperty); 1747 1748 return retVal; 1749 } 1750 1751 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 1752 private void handleFilterLoincDescendant( 1753 String theSystem, 1754 SearchPredicateFactory f, 1755 BooleanPredicateClausesStep<?> b, 1756 ValueSet.ConceptSetFilterComponent theFilter) { 1757 switch (theFilter.getOp()) { 1758 case EQUAL: 1759 addLoincFilterDescendantEqual(theSystem, f, b, theFilter); 1760 break; 1761 case IN: 1762 addLoincFilterDescendantIn(theSystem, f, b, theFilter); 1763 break; 1764 default: 1765 throw new InvalidRequestException(Msg.code(896) + "Don't know how to handle op=" + theFilter.getOp() 1766 + " on property " + theFilter.getProperty()); 1767 } 1768 } 1769 1770 private void addLoincFilterDescendantEqual( 1771 String theSystem, 1772 SearchPredicateFactory f, 1773 BooleanPredicateClausesStep<?> b, 1774 ValueSet.ConceptSetFilterComponent theFilter) { 1775 1776 List<TermConcept.TermConceptPk> parentPids = 1777 getCodeParentPids(theSystem, theFilter.getProperty(), theFilter.getValue()); 1778 if (parentPids.isEmpty()) { 1779 // Can't return empty must, because it wil match according to other predicates. 1780 // Some day there will be a 'matchNone' predicate 1781 // (https://discourse.hibernate.org/t/fail-fast-predicate/6062) 1782 b.mustNot(f.matchAll()); 1783 return; 1784 } 1785 1786 b.must(f.bool(innerB -> { 1787 innerB.minimumShouldMatchNumber(1); 1788 parentPids.forEach(pid -> innerB.should(f.match().field("myId").matching(pid))); 1789 })); 1790 } 1791 1792 /** 1793 * We are looking for codes which have codes indicated in theFilter.getValue() as descendants. 1794 * Strategy is to find codes which have their pId(s) in the list of the parentId(s) of all the TermConcept(s) 1795 * representing the codes in theFilter.getValue() 1796 */ 1797 private void addLoincFilterDescendantIn( 1798 String theSystem, 1799 SearchPredicateFactory f, 1800 BooleanPredicateClausesStep<?> b, 1801 ValueSet.ConceptSetFilterComponent theFilter) { 1802 1803 String[] values = theFilter.getValue().split(","); 1804 if (values.length == 0) { 1805 throw new InvalidRequestException(Msg.code(2062) + "Invalid filter criteria - no codes specified"); 1806 } 1807 1808 List<TermConcept.TermConceptPk> descendantCodePidList = 1809 getMultipleCodeParentPids(theSystem, theFilter.getProperty(), values); 1810 1811 b.must(f.bool(innerB -> descendantCodePidList.forEach( 1812 pId -> innerB.should(f.match().field("myId").matching(pId))))); 1813 } 1814 1815 /** 1816 * Returns the list of parentId(s) of the TermConcept representing theValue as a code 1817 */ 1818 private List<TermConcept.TermConceptPk> getCodeParentPids(String theSystem, String theProperty, String theValue) { 1819 TermConcept code = findCode(theSystem, theValue) 1820 .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {" 1821 + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue)); 1822 1823 String[] parentPids = code.getParentPidsAsString().split(" "); 1824 List<TermConcept.TermConceptPk> retVal = Arrays.stream(parentPids) 1825 .filter(pid -> !StringUtils.equals(pid, "NONE")) 1826 .map(Long::parseLong) 1827 .map(t -> new TermConcept.TermConceptPk(t, myPartitionSettings.getDefaultPartitionId())) 1828 .collect(Collectors.toList()); 1829 logFilteringValueOnProperty(theValue, theProperty); 1830 return retVal; 1831 } 1832 1833 /** 1834 * Returns the list of parentId(s) of the TermConcept representing theValue as a code 1835 */ 1836 private List<TermConcept.TermConceptPk> getMultipleCodeParentPids( 1837 String theSystem, String theProperty, String[] theValues) { 1838 List<String> valuesList = Arrays.asList(theValues); 1839 List<TermConcept> termConcepts = findCodes(theSystem, valuesList); 1840 if (valuesList.size() != termConcepts.size()) { 1841 String exMsg = getTermConceptsFetchExceptionMsg(termConcepts, valuesList); 1842 throw new InvalidRequestException(Msg.code(2064) + "Invalid filter criteria - {" 1843 + Constants.codeSystemWithDefaultDescription(theSystem) + "}: " + exMsg); 1844 } 1845 1846 List<TermConcept.TermConceptPk> retVal = termConcepts.stream() 1847 .flatMap(tc -> Arrays.stream(tc.getParentPidsAsString().split(" "))) 1848 .filter(pid -> !StringUtils.equals(pid, "NONE")) 1849 .map(Long::parseLong) 1850 .map(t -> new TermConcept.TermConceptPk(t, myPartitionSettings.getDefaultPartitionId())) 1851 .collect(Collectors.toList()); 1852 1853 logFilteringValueOnProperties(valuesList, theProperty); 1854 1855 return retVal; 1856 } 1857 1858 /** 1859 * Generate message indicating for which of theValues a TermConcept was not found 1860 */ 1861 private String getTermConceptsFetchExceptionMsg(List<TermConcept> theTermConcepts, List<String> theValues) { 1862 // case: more TermConcept(s) retrieved than codes queried 1863 if (theTermConcepts.size() > theValues.size()) { 1864 return "Invalid filter criteria - More TermConcepts were found than indicated codes. Queried codes: [" 1865 + join( 1866 ",", 1867 theValues + "]; Obtained TermConcept IDs, codes: [" 1868 + theTermConcepts.stream() 1869 .map(tc -> tc.getId() + ", " + tc.getCode()) 1870 .collect(joining("; ")) 1871 + "]"); 1872 } 1873 1874 // case: less TermConcept(s) retrieved than codes queried 1875 Set<String> matchedCodes = 1876 theTermConcepts.stream().map(TermConcept::getCode).collect(toSet()); 1877 List<String> notMatchedValues = 1878 theValues.stream().filter(v -> !matchedCodes.contains(v)).collect(toList()); 1879 1880 return "Invalid filter criteria - No TermConcept(s) were found for the requested codes: [" 1881 + join(",", notMatchedValues + "]"); 1882 } 1883 1884 private void logFilteringValueOnProperty(String theValue, String theProperty) { 1885 ourLog.debug(" * Filtering with value={} on property {}", theValue, theProperty); 1886 } 1887 1888 private void logFilteringValueOnProperties(List<String> theValues, String theProperty) { 1889 ourLog.debug(" * Filtering with values={} on property {}", String.join(", ", theValues), theProperty); 1890 } 1891 1892 private void throwInvalidRequestForOpOnProperty(ValueSet.FilterOperator theOp, String theProperty) { 1893 throw new InvalidRequestException( 1894 Msg.code(897) + "Don't know how to handle op=" + theOp + " on property " + theProperty); 1895 } 1896 1897 private void throwInvalidRequestForValueOnProperty(String theValue, String theProperty) { 1898 throw new InvalidRequestException( 1899 Msg.code(898) + "Don't know how to handle value=" + theValue + " on property " + theProperty); 1900 } 1901 1902 private void expandWithoutHibernateSearch( 1903 IValueSetConceptAccumulator theValueSetCodeAccumulator, 1904 TermCodeSystemVersion theVersion, 1905 Set<String> theAddedCodes, 1906 ValueSet.ConceptSetComponent theInclude, 1907 String theSystem, 1908 boolean theAdd) { 1909 ourLog.trace("Hibernate search is not enabled"); 1910 1911 if (theValueSetCodeAccumulator instanceof ValueSetExpansionComponentWithConceptAccumulator) { 1912 Validate.isTrue( 1913 ((ValueSetExpansionComponentWithConceptAccumulator) theValueSetCodeAccumulator) 1914 .getParameter() 1915 .isEmpty(), 1916 "Can not expand ValueSet with parameters - Hibernate Search is not enabled on this server."); 1917 } 1918 1919 Validate.isTrue( 1920 isNotBlank(theSystem), 1921 "Can not expand ValueSet without explicit system - Hibernate Search is not enabled on this server."); 1922 1923 for (ValueSet.ConceptSetFilterComponent nextFilter : theInclude.getFilter()) { 1924 boolean handled = false; 1925 switch (nextFilter.getProperty().toLowerCase()) { 1926 case "concept": 1927 case "code": 1928 if (nextFilter.getOp() == ValueSet.FilterOperator.ISA) { 1929 theValueSetCodeAccumulator.addMessage( 1930 "Processing IS-A filter in database - Note that Hibernate Search is not enabled on this server, so this operation can be inefficient."); 1931 TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, nextFilter); 1932 addConceptAndChildren( 1933 theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, code); 1934 handled = true; 1935 } 1936 break; 1937 default: 1938 // TODO - we need to handle other properties (fields) 1939 // and other operations (not just is-a) 1940 // in some (preferably generic) way 1941 break; 1942 } 1943 1944 if (!handled) { 1945 throwInvalidFilter( 1946 nextFilter, 1947 " - Note that Hibernate Search is disabled on this server so not all ValueSet expansion functionality is available."); 1948 } 1949 } 1950 1951 if (theInclude.getFilter().isEmpty() && theInclude.getConcept().isEmpty()) { 1952 Collection<TermConcept> concepts = 1953 myConceptDao.fetchConceptsAndDesignationsByVersionPid(theVersion.getPid()); 1954 for (TermConcept next : concepts) { 1955 addCodeIfNotAlreadyAdded( 1956 theValueSetCodeAccumulator, 1957 theAddedCodes, 1958 theAdd, 1959 theSystem, 1960 theInclude.getVersion(), 1961 next.getCode(), 1962 next.getDisplay(), 1963 next.getId(), 1964 next.getParentPidsAsString(), 1965 next.getDesignations()); 1966 } 1967 } 1968 1969 for (ValueSet.ConceptReferenceComponent next : theInclude.getConcept()) { 1970 if (!theSystem.equals(theInclude.getSystem()) && isNotBlank(theSystem)) { 1971 continue; 1972 } 1973 Collection<TermConceptDesignation> designations = next.getDesignation().stream() 1974 .map(t -> new TermConceptDesignation() 1975 .setValue(t.getValue()) 1976 .setLanguage(t.getLanguage()) 1977 .setUseCode(t.getUse().getCode()) 1978 .setUseSystem(t.getUse().getSystem()) 1979 .setUseDisplay(t.getUse().getDisplay())) 1980 .collect(Collectors.toList()); 1981 addCodeIfNotAlreadyAdded( 1982 theValueSetCodeAccumulator, 1983 theAddedCodes, 1984 theAdd, 1985 theSystem, 1986 theInclude.getVersion(), 1987 next.getCode(), 1988 next.getDisplay(), 1989 null, 1990 null, 1991 designations); 1992 } 1993 } 1994 1995 private void addConceptAndChildren( 1996 IValueSetConceptAccumulator theValueSetCodeAccumulator, 1997 Set<String> theAddedCodes, 1998 ValueSet.ConceptSetComponent theInclude, 1999 String theSystem, 2000 boolean theAdd, 2001 TermConcept theConcept) { 2002 for (TermConcept nextChild : theConcept.getChildCodes()) { 2003 boolean added = addCodeIfNotAlreadyAdded( 2004 theValueSetCodeAccumulator, 2005 theAddedCodes, 2006 theAdd, 2007 theSystem, 2008 theInclude.getVersion(), 2009 nextChild.getCode(), 2010 nextChild.getDisplay(), 2011 nextChild.getId(), 2012 nextChild.getParentPidsAsString(), 2013 nextChild.getDesignations()); 2014 if (added) { 2015 addConceptAndChildren( 2016 theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, nextChild); 2017 } 2018 } 2019 } 2020 2021 @Override 2022 @Transactional 2023 public String invalidatePreCalculatedExpansion(IIdType theValueSetId, RequestDetails theRequestDetails) { 2024 IBaseResource valueSet = myDaoRegistry.getResourceDao("ValueSet").read(theValueSetId, theRequestDetails); 2025 ValueSet canonicalValueSet = myVersionCanonicalizer.valueSetToCanonical(valueSet); 2026 Optional<TermValueSet> optionalTermValueSet = fetchValueSetEntity(canonicalValueSet); 2027 if (optionalTermValueSet.isEmpty()) { 2028 return myContext 2029 .getLocalizer() 2030 .getMessage(TermReadSvcImpl.class, "valueSetNotFoundInTerminologyDatabase", theValueSetId); 2031 } 2032 2033 ourLog.info( 2034 "Invalidating pre-calculated expansion on ValueSet {} / {}", theValueSetId, canonicalValueSet.getUrl()); 2035 2036 TermValueSet termValueSet = optionalTermValueSet.get(); 2037 if (termValueSet.getExpansionStatus() == TermValueSetPreExpansionStatusEnum.NOT_EXPANDED) { 2038 return myContext 2039 .getLocalizer() 2040 .getMessage( 2041 TermReadSvcImpl.class, 2042 "valueSetCantInvalidateNotYetPrecalculated", 2043 termValueSet.getUrl(), 2044 termValueSet.getExpansionStatus()); 2045 } 2046 2047 Long totalConcepts = termValueSet.getTotalConcepts(); 2048 2049 deletePreCalculatedValueSetContents(termValueSet); 2050 2051 termValueSet.setExpansionStatus(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED); 2052 termValueSet.setExpansionTimestamp(null); 2053 2054 assert termValueSet.getId() != null; 2055 myEntityManager.merge(termValueSet); 2056 2057 afterValueSetExpansionStatusChange(); 2058 2059 return myContext 2060 .getLocalizer() 2061 .getMessage( 2062 TermReadSvcImpl.class, "valueSetPreExpansionInvalidated", termValueSet.getUrl(), totalConcepts); 2063 } 2064 2065 @Override 2066 @Transactional(readOnly = true) 2067 public boolean isValueSetPreExpandedForCodeValidation(ValueSet theValueSet) { 2068 Optional<TermValueSet> optionalTermValueSet = fetchValueSetEntity(theValueSet); 2069 2070 if (optionalTermValueSet.isEmpty()) { 2071 ourLog.warn( 2072 "ValueSet is not present in terminology tables. Will perform in-memory code validation. {}", 2073 getValueSetInfo(theValueSet)); 2074 return false; 2075 } 2076 2077 TermValueSet termValueSet = optionalTermValueSet.get(); 2078 2079 if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) { 2080 ourLog.warn( 2081 "{} is present in terminology tables but not ready for persistence-backed invocation of operation $validation-code. Will perform in-memory code validation. Current status: {} | {}", 2082 getValueSetInfo(theValueSet), 2083 termValueSet.getExpansionStatus().name(), 2084 termValueSet.getExpansionStatus().getDescription()); 2085 return false; 2086 } 2087 2088 return true; 2089 } 2090 2091 @SuppressWarnings({"OptionalAssignedToNull", "unchecked"}) 2092 private Optional<TermValueSet> fetchValueSetEntity(ValueSet theValueSet) { 2093 Optional<TermValueSet> retVal = (Optional<TermValueSet>) theValueSet.getUserData(VS_USERDATA_CURRENT_VERSION); 2094 if (retVal == null) { 2095 synchronized (theValueSet) { 2096 JpaPid valueSetResourcePid = getValueSetResourcePersistentId(theValueSet); 2097 retVal = myTermValueSetDao.findByResourcePid(valueSetResourcePid); 2098 theValueSet.setUserData(VS_USERDATA_CURRENT_VERSION, retVal); 2099 } 2100 } 2101 2102 return retVal; 2103 } 2104 2105 private JpaPid getValueSetResourcePersistentId(ValueSet theValueSet) { 2106 return myIdHelperService.resolveResourceIdentityPid( 2107 RequestPartitionId.allPartitions(), 2108 theValueSet.getIdElement().getResourceType(), 2109 theValueSet.getIdElement().getIdPart(), 2110 ResolveIdentityMode.includeDeleted().cacheOk()); 2111 } 2112 2113 protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedValueSet( 2114 ValidationSupportContext theValidationSupportContext, 2115 ConceptValidationOptions theValidationOptions, 2116 ValueSet theValueSet, 2117 String theSystem, 2118 String theCode, 2119 String theDisplay, 2120 Coding theCoding, 2121 CodeableConcept theCodeableConcept) { 2122 assert TransactionSynchronizationManager.isSynchronizationActive(); 2123 2124 ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet.hasId(), "ValueSet.id is required"); 2125 JpaPid valueSetResourcePid = getValueSetResourcePersistentId(theValueSet); 2126 2127 List<TermValueSetConcept> concepts = new ArrayList<>(); 2128 if (isNotBlank(theCode)) { 2129 if (theValidationOptions.isInferSystem()) { 2130 concepts.addAll( 2131 myValueSetConceptDao.findByValueSetResourcePidAndCode(valueSetResourcePid.getId(), theCode)); 2132 } else if (isNotBlank(theSystem)) { 2133 concepts.addAll(findByValueSetResourcePidSystemAndCode(valueSetResourcePid, theSystem, theCode)); 2134 } 2135 } else if (theCoding != null) { 2136 if (theCoding.hasSystem() && theCoding.hasCode()) { 2137 concepts.addAll(findByValueSetResourcePidSystemAndCode( 2138 valueSetResourcePid, theCoding.getSystem(), theCoding.getCode())); 2139 } 2140 } else if (theCodeableConcept != null) { 2141 for (Coding coding : theCodeableConcept.getCoding()) { 2142 if (coding.hasSystem() && coding.hasCode()) { 2143 concepts.addAll(findByValueSetResourcePidSystemAndCode( 2144 valueSetResourcePid, coding.getSystem(), coding.getCode())); 2145 if (!concepts.isEmpty()) { 2146 break; 2147 } 2148 } 2149 } 2150 } else { 2151 return null; 2152 } 2153 2154 TermValueSet valueSetEntity = fetchValueSetEntity(theValueSet).orElseThrow(IllegalStateException::new); 2155 String timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity); 2156 String preExpansionMessage = myContext 2157 .getLocalizer() 2158 .getMessage(TermReadSvcImpl.class, "validationPerformedAgainstPreExpansion", timingDescription); 2159 2160 if (theValidationOptions.isValidateDisplay() && concepts.size() > 0) { 2161 String systemVersion = null; 2162 for (TermValueSetConcept concept : concepts) { 2163 systemVersion = concept.getSystemVersion(); 2164 if (isBlank(theDisplay) || isBlank(concept.getDisplay()) || theDisplay.equals(concept.getDisplay())) { 2165 return new IValidationSupport.CodeValidationResult() 2166 .setCode(concept.getCode()) 2167 .setDisplay(concept.getDisplay()) 2168 .setCodeSystemVersion(concept.getSystemVersion()) 2169 .setSourceDetails(preExpansionMessage); 2170 } 2171 } 2172 2173 String expectedDisplay = concepts.get(0).getDisplay(); 2174 return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch( 2175 myContext, 2176 theCode, 2177 theDisplay, 2178 expectedDisplay, 2179 theSystem, 2180 systemVersion, 2181 myStorageSettings.getIssueSeverityForCodeDisplayMismatch()); 2182 } 2183 2184 if (!concepts.isEmpty()) { 2185 return new IValidationSupport.CodeValidationResult() 2186 .setCode(concepts.get(0).getCode()) 2187 .setDisplay(concepts.get(0).getDisplay()) 2188 .setCodeSystemVersion(concepts.get(0).getSystemVersion()) 2189 .setMessage(preExpansionMessage); 2190 } 2191 2192 // Ok, we failed 2193 List<TermValueSetConcept> outcome = myValueSetConceptDao.findByTermValueSetIdSystemOnly( 2194 Pageable.ofSize(1), valueSetEntity.getId(), theSystem); 2195 String append; 2196 if (outcome.size() == 0) { 2197 append = " - No codes in ValueSet belong to CodeSystem with URL " + theSystem; 2198 } else { 2199 String unknownCodeMessage = myContext 2200 .getLocalizer() 2201 .getMessage(TermReadSvcImpl.class, "unknownCodeInSystem", theSystem, theCode); 2202 append = " - " + unknownCodeMessage + ". " + preExpansionMessage; 2203 } 2204 2205 return createCodeNotFoundErrorForValidationResult(theSystem, theCode, null, append); 2206 } 2207 2208 private CodeValidationResult createCodeNotFoundErrorForValidationResult( 2209 String theSystem, String theCode, String theCodeSystemVersion, String theAppend) { 2210 String theMessage = "Unable to validate code " + theSystem + "#" + theCode + theAppend; 2211 // The InstanceValidator (core) will change the severity based on the binding strength 2212 return new CodeValidationResult() 2213 .setSeverity(IssueSeverity.ERROR) 2214 .setCodeSystemVersion(theCodeSystemVersion) 2215 .setMessage(theMessage) 2216 .addIssue(new CodeValidationIssue( 2217 theMessage, 2218 IssueSeverity.ERROR, 2219 CodeValidationIssueCode.NOT_FOUND, 2220 CodeValidationIssueCoding.NOT_FOUND)); 2221 } 2222 2223 private List<TermValueSetConcept> findByValueSetResourcePidSystemAndCode( 2224 JpaPid theResourcePid, String theSystem, String theCode) { 2225 assert TransactionSynchronizationManager.isSynchronizationActive(); 2226 2227 List<TermValueSetConcept> retVal = new ArrayList<>(); 2228 Optional<TermValueSetConcept> optionalTermValueSetConcept; 2229 int versionIndex = theSystem.indexOf(OUR_PIPE_CHARACTER); 2230 if (versionIndex >= 0) { 2231 String systemUrl = theSystem.substring(0, versionIndex); 2232 String systemVersion = theSystem.substring(versionIndex + 1); 2233 optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCodeWithVersion( 2234 theResourcePid.getId(), systemUrl, systemVersion, theCode); 2235 } else { 2236 optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCode( 2237 theResourcePid.getId(), theSystem, theCode); 2238 } 2239 optionalTermValueSetConcept.ifPresent(retVal::add); 2240 return retVal; 2241 } 2242 2243 private void fetchChildren(TermConcept theConcept, Set<TermConcept> theSetToPopulate) { 2244 for (TermConceptParentChildLink nextChildLink : theConcept.getChildren()) { 2245 TermConcept nextChild = nextChildLink.getChild(); 2246 if (addToSet(theSetToPopulate, nextChild)) { 2247 fetchChildren(nextChild, theSetToPopulate); 2248 } 2249 } 2250 } 2251 2252 private Optional<TermConcept> fetchLoadedCode(Long theCodeSystemResourcePid, String theCode) { 2253 TermCodeSystemVersion codeSystem = 2254 myCodeSystemVersionDao.findCurrentVersionForCodeSystemResourcePid(theCodeSystemResourcePid); 2255 return myConceptDao.findByCodeSystemAndCode(codeSystem.getPid(), theCode); 2256 } 2257 2258 private void fetchParents(TermConcept theConcept, Set<TermConcept> theSetToPopulate) { 2259 for (TermConceptParentChildLink nextChildLink : theConcept.getParents()) { 2260 TermConcept nextChild = nextChildLink.getParent(); 2261 if (addToSet(theSetToPopulate, nextChild)) { 2262 fetchParents(nextChild, theSetToPopulate); 2263 } 2264 } 2265 } 2266 2267 @Override 2268 public Optional<TermConcept> findCode(String theCodeSystem, String theCode) { 2269 /* 2270 * Loading concepts without a transaction causes issues later on some 2271 * platforms (e.g. PSQL) so make sure that we always call this with an open transaction 2272 */ 2273 HapiTransactionService.requireTransaction(); 2274 2275 TermCodeSystemVersionDetails csv = 2276 getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem); 2277 if (csv == null) { 2278 return Optional.empty(); 2279 } 2280 return myConceptDao.findByCodeSystemAndCode(csv.myPid, theCode); 2281 } 2282 2283 @Override 2284 public List<TermConcept> findCodes(String theCodeSystem, List<String> theCodeList) { 2285 HapiTransactionService.requireTransaction(); 2286 2287 TermCodeSystemVersionDetails csv = 2288 getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem); 2289 if (csv == null) { 2290 return Collections.emptyList(); 2291 } 2292 2293 return myConceptDao.findByCodeSystemAndCodeList(csv.myPid, theCodeList); 2294 } 2295 2296 @Nullable 2297 private TermCodeSystemVersionDetails getCurrentCodeSystemVersion( 2298 ValidationSupportContext theValidationSupportContext, String theCodeSystemIdentifier) { 2299 String version = getVersionFromIdentifier(theCodeSystemIdentifier); 2300 2301 // Fetch the CodeSystem from ValidationSupport, which should return a cached copy. We 2302 // keep a copy of the current version entity in userData in that cached copy 2303 // to avoid repeated lookups 2304 TermCodeSystemVersionDetails retVal; 2305 IBaseResource codeSystem = 2306 theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(theCodeSystemIdentifier); 2307 if (codeSystem != null) { 2308 2309 synchronized (codeSystem) { 2310 retVal = (TermCodeSystemVersionDetails) codeSystem.getUserData(CS_USERDATA_CURRENT_VERSION); 2311 if (retVal == null) { 2312 retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version); 2313 codeSystem.setUserData(CS_USERDATA_CURRENT_VERSION, retVal); 2314 } 2315 } 2316 } else { 2317 retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version); 2318 } 2319 2320 return retVal; 2321 } 2322 2323 @Nullable 2324 private TermCodeSystemVersionDetails getCurrentCodeSystemVersion(String theCodeSystemIdentifier, String version) { 2325 TermCodeSystemVersionDetails retVal; 2326 retVal = myTxTemplate.execute(tx -> { 2327 TermCodeSystemVersion csv = null; 2328 TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(getUrlFromIdentifier(theCodeSystemIdentifier)); 2329 if (cs != null) { 2330 if (version != null) { 2331 csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), version); 2332 } else if (cs.getCurrentVersion() != null) { 2333 csv = cs.getCurrentVersion(); 2334 } 2335 } 2336 if (csv != null) { 2337 return new TermCodeSystemVersionDetails(csv.getPid(), csv.getCodeSystemVersionId()); 2338 } else { 2339 return null; 2340 } 2341 }); 2342 return retVal; 2343 } 2344 2345 private String getVersionFromIdentifier(String theUri) { 2346 String retVal = null; 2347 if (StringUtils.isNotEmpty((theUri))) { 2348 int versionSeparator = theUri.lastIndexOf('|'); 2349 if (versionSeparator != -1) { 2350 retVal = theUri.substring(versionSeparator + 1); 2351 } 2352 } 2353 return retVal; 2354 } 2355 2356 private String getUrlFromIdentifier(String theUri) { 2357 String retVal = theUri; 2358 if (StringUtils.isNotEmpty((theUri))) { 2359 int versionSeparator = theUri.lastIndexOf('|'); 2360 if (versionSeparator != -1) { 2361 retVal = theUri.substring(0, versionSeparator); 2362 } 2363 } 2364 return retVal; 2365 } 2366 2367 @Transactional(propagation = Propagation.REQUIRED, readOnly = true) 2368 @Override 2369 public Set<TermConcept> findCodesAbove( 2370 Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) { 2371 StopWatch stopwatch = new StopWatch(); 2372 2373 Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode); 2374 if (concept.isEmpty()) { 2375 return Collections.emptySet(); 2376 } 2377 2378 Set<TermConcept> retVal = new HashSet<>(); 2379 retVal.add(concept.get()); 2380 2381 fetchParents(concept.get(), retVal); 2382 2383 ourLog.debug("Fetched {} codes above code {} in {}ms", retVal.size(), theCode, stopwatch.getMillis()); 2384 return retVal; 2385 } 2386 2387 @Transactional(readOnly = true) 2388 @Override 2389 public List<FhirVersionIndependentConcept> findCodesAbove(String theSystem, String theCode) { 2390 TermCodeSystem cs = getCodeSystem(theSystem); 2391 if (cs == null) { 2392 return findCodesAboveUsingBuiltInSystems(theSystem, theCode); 2393 } 2394 TermCodeSystemVersion csv = cs.getCurrentVersion(); 2395 2396 Set<TermConcept> codes = findCodesAbove(cs.getResource().getId().getId(), csv.getPid(), theCode); 2397 return toVersionIndependentConcepts(theSystem, codes); 2398 } 2399 2400 @Transactional(propagation = Propagation.REQUIRED, readOnly = true) 2401 @Override 2402 public Set<TermConcept> findCodesBelow( 2403 Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) { 2404 Stopwatch stopwatch = Stopwatch.createStarted(); 2405 2406 Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode); 2407 if (concept.isEmpty()) { 2408 return Collections.emptySet(); 2409 } 2410 2411 Set<TermConcept> retVal = new HashSet<>(); 2412 retVal.add(concept.get()); 2413 2414 fetchChildren(concept.get(), retVal); 2415 2416 ourLog.debug( 2417 "Fetched {} codes below code {} in {}ms", 2418 retVal.size(), 2419 theCode, 2420 stopwatch.elapsed(TimeUnit.MILLISECONDS)); 2421 return retVal; 2422 } 2423 2424 @Transactional(readOnly = true) 2425 @Override 2426 public List<FhirVersionIndependentConcept> findCodesBelow(String theSystem, String theCode) { 2427 TermCodeSystem cs = getCodeSystem(theSystem); 2428 if (cs == null) { 2429 return findCodesBelowUsingBuiltInSystems(theSystem, theCode); 2430 } 2431 TermCodeSystemVersion csv = cs.getCurrentVersion(); 2432 2433 Set<TermConcept> codes = findCodesBelow(cs.getResource().getId().getId(), csv.getPid(), theCode); 2434 return toVersionIndependentConcepts(theSystem, codes); 2435 } 2436 2437 private TermCodeSystem getCodeSystem(String theSystem) { 2438 return myCodeSystemDao.findByCodeSystemUri(theSystem); 2439 } 2440 2441 @PostConstruct 2442 public void start() { 2443 RuleBasedTransactionAttribute rules = new RuleBasedTransactionAttribute(); 2444 rules.getRollbackRules().add(new NoRollbackRuleAttribute(ExpansionTooCostlyException.class)); 2445 myTxTemplate = new TransactionTemplate(myTransactionManager, rules); 2446 } 2447 2448 @Override 2449 public void scheduleJobs(ISchedulerService theSchedulerService) { 2450 // Register scheduled job to pre-expand ValueSets 2451 // In the future it would be great to make this a cluster-aware task somehow 2452 ScheduledJobDefinition vsJobDefinition = new ScheduledJobDefinition(); 2453 vsJobDefinition.setId(getClass().getName()); 2454 vsJobDefinition.setJobClass(Job.class); 2455 theSchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_MINUTE, vsJobDefinition); 2456 } 2457 2458 @Override 2459 public synchronized void preExpandDeferredValueSetsToTerminologyTables() { 2460 if (!myStorageSettings.isEnableTaskPreExpandValueSets()) { 2461 return; 2462 } 2463 if (isNotSafeToPreExpandValueSets()) { 2464 ourLog.info("Skipping scheduled pre-expansion of ValueSets while deferred entities are being loaded."); 2465 return; 2466 } 2467 TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); 2468 2469 while (true) { 2470 StopWatch sw = new StopWatch(); 2471 TermValueSet valueSetToExpand = txTemplate.execute(t -> { 2472 Optional<TermValueSet> optionalTermValueSet = getNextTermValueSetNotExpanded(); 2473 if (optionalTermValueSet.isEmpty()) { 2474 return null; 2475 } 2476 2477 TermValueSet termValueSet = optionalTermValueSet.get(); 2478 termValueSet.setTotalConcepts(0L); 2479 termValueSet.setTotalConceptDesignations(0L); 2480 termValueSet.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANSION_IN_PROGRESS); 2481 TermValueSet retVal = myEntityManager.merge(termValueSet); 2482 myEntityManager.flush(); 2483 return retVal; 2484 }); 2485 if (valueSetToExpand == null) { 2486 return; 2487 } 2488 2489 // We have a ValueSet to pre-expand. 2490 setPreExpandingValueSets(true); 2491 try { 2492 ValueSet valueSet = txTemplate.execute(t -> { 2493 TermValueSet refreshedValueSetToExpand = myTermValueSetDao 2494 .findById(valueSetToExpand.getPartitionedId()) 2495 .orElseThrow(() -> new IllegalStateException("Unknown VS ID: " + valueSetToExpand.getId())); 2496 return getValueSetFromResourceTable(refreshedValueSetToExpand.getResource()); 2497 }); 2498 assert valueSet != null; 2499 2500 ValueSetConceptAccumulator valueSetConceptAccumulator = 2501 myValueSetConceptAccumulatorFactory.create(valueSetToExpand); 2502 ValueSetExpansionOptions options = new ValueSetExpansionOptions(); 2503 options.setIncludeHierarchy(true); 2504 expandValueSet(options, valueSet, valueSetConceptAccumulator); 2505 2506 // We are done with this ValueSet. 2507 txTemplate.executeWithoutResult(t -> { 2508 valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANDED); 2509 valueSetToExpand.setExpansionTimestamp(new Date()); 2510 myEntityManager.merge(valueSetToExpand); 2511 }); 2512 2513 afterValueSetExpansionStatusChange(); 2514 2515 ourLog.info( 2516 "Pre-expanded ValueSet[{}] with URL[{}] - Saved {} concepts in {}", 2517 valueSet.getId(), 2518 valueSet.getUrl(), 2519 valueSetConceptAccumulator.getConceptsSaved(), 2520 sw); 2521 2522 } catch (Exception e) { 2523 ourLog.error( 2524 "Failed to pre-expand ValueSet with URL[{}]: {}", valueSetToExpand.getUrl(), e.getMessage(), e); 2525 txTemplate.executeWithoutResult(t -> { 2526 valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.FAILED_TO_EXPAND); 2527 myEntityManager.merge(valueSetToExpand); 2528 }); 2529 2530 } finally { 2531 setPreExpandingValueSets(false); 2532 } 2533 } 2534 } 2535 2536 /* 2537 * If a ValueSet has just finished pre-expanding, let's flush the caches. This is 2538 * kind of a blunt tool, but it should ensure that users don't get unpredictable 2539 * results while they test changes, which is probably a worthwhile sacrifice 2540 */ 2541 private void afterValueSetExpansionStatusChange() { 2542 provideValidationSupport().invalidateCaches(); 2543 } 2544 2545 @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") 2546 @Override 2547 public void invalidateCaches() { 2548 /* 2549 * Clear out anything left in the userdata caches. We do this mostly because it messes 2550 * up unit tests to have these things stick around between test runs, since many of 2551 * these resources come from DefaultProfileValidationSupport and therefore live beyond 2552 * any single test execution. 2553 */ 2554 for (IBaseResource next : provideValidationSupport().fetchAllConformanceResources()) { 2555 if (next != null) { 2556 synchronized (next) { 2557 if (next.getUserData(CS_USERDATA_CURRENT_VERSION) != null) { 2558 next.setUserData(CS_USERDATA_CURRENT_VERSION, null); 2559 } 2560 if (next.getUserData(VS_USERDATA_CURRENT_VERSION) != null) { 2561 next.setUserData(VS_USERDATA_CURRENT_VERSION, null); 2562 } 2563 } 2564 } 2565 } 2566 } 2567 2568 private synchronized boolean isPreExpandingValueSets() { 2569 return myPreExpandingValueSets; 2570 } 2571 2572 private synchronized void setPreExpandingValueSets(boolean thePreExpandingValueSets) { 2573 myPreExpandingValueSets = thePreExpandingValueSets; 2574 } 2575 2576 private boolean isNotSafeToPreExpandValueSets() { 2577 return myDeferredStorageSvc != null && !myDeferredStorageSvc.isStorageQueueEmpty(true); 2578 } 2579 2580 private Optional<TermValueSet> getNextTermValueSetNotExpanded() { 2581 Optional<TermValueSet> retVal = Optional.empty(); 2582 Slice<TermValueSet> page = myTermValueSetDao.findByExpansionStatus( 2583 PageRequest.of(0, 1), TermValueSetPreExpansionStatusEnum.NOT_EXPANDED); 2584 2585 if (!page.getContent().isEmpty()) { 2586 retVal = Optional.of(page.getContent().get(0)); 2587 } 2588 2589 return retVal; 2590 } 2591 2592 @Override 2593 @Transactional 2594 public void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet) { 2595 // If we're in a transaction, we need to flush now so that we can correctly detect 2596 // duplicates if there are multiple ValueSets in the same TX with the same URL 2597 // (which is an error, but we need to catch it). It'd be better to catch this by 2598 // inspecting the URLs in the bundle or something, since flushing hurts performance 2599 // but it's not expected that loading valuesets is going to be a huge high frequency 2600 // thing so it probably doesn't matter 2601 myEntityManager.flush(); 2602 2603 ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied"); 2604 if (isPlaceholder(theValueSet)) { 2605 ourLog.info( 2606 "Not storing TermValueSet for placeholder {}", 2607 theValueSet.getIdElement().toVersionless().getValueAsString()); 2608 return; 2609 } 2610 2611 ValidateUtil.isNotBlankOrThrowUnprocessableEntity( 2612 theValueSet.getUrl(), "ValueSet has no value for ValueSet.url"); 2613 ourLog.info( 2614 "Storing TermValueSet for {}", 2615 theValueSet.getIdElement().toVersionless().getValueAsString()); 2616 2617 /* 2618 * Get CodeSystem and validate CodeSystemVersion 2619 */ 2620 TermValueSet termValueSet = new TermValueSet(); 2621 termValueSet.setResource(theResourceTable); 2622 termValueSet.setUrl(theValueSet.getUrl()); 2623 termValueSet.setVersion(theValueSet.getVersion()); 2624 termValueSet.setName(theValueSet.hasName() ? theValueSet.getName() : null); 2625 2626 // Delete version being replaced 2627 Optional<TermValueSet> deletedTrmValueSet = deleteValueSetForResource(theResourceTable); 2628 2629 /* 2630 * Do the upload. 2631 */ 2632 String url = termValueSet.getUrl(); 2633 String version = termValueSet.getVersion(); 2634 Optional<TermValueSet> optionalExistingTermValueSetByUrl; 2635 2636 if (deletedTrmValueSet.isPresent() 2637 && Objects.equals(deletedTrmValueSet.get().getUrl(), url) 2638 && Objects.equals(deletedTrmValueSet.get().getVersion(), version)) { 2639 // If we just deleted the valueset marker, we don't need to check if it exists 2640 // in the database 2641 optionalExistingTermValueSetByUrl = Optional.empty(); 2642 } else { 2643 optionalExistingTermValueSetByUrl = getTermValueSet(version, url); 2644 } 2645 2646 if (optionalExistingTermValueSetByUrl.isEmpty()) { 2647 2648 myEntityManager.persist(termValueSet); 2649 2650 } else { 2651 TermValueSet existingTermValueSet = optionalExistingTermValueSetByUrl.get(); 2652 String msg; 2653 if (version != null) { 2654 msg = myContext 2655 .getLocalizer() 2656 .getMessage( 2657 TermReadSvcImpl.class, 2658 "cannotCreateDuplicateValueSetUrlAndVersion", 2659 url, 2660 version, 2661 existingTermValueSet 2662 .getResource() 2663 .getIdDt() 2664 .toUnqualifiedVersionless() 2665 .getValue()); 2666 } else { 2667 msg = myContext 2668 .getLocalizer() 2669 .getMessage( 2670 TermReadSvcImpl.class, 2671 "cannotCreateDuplicateValueSetUrl", 2672 url, 2673 existingTermValueSet 2674 .getResource() 2675 .getIdDt() 2676 .toUnqualifiedVersionless() 2677 .getValue()); 2678 } 2679 throw new UnprocessableEntityException(Msg.code(902) + msg); 2680 } 2681 } 2682 2683 private Optional<TermValueSet> getTermValueSet(String version, String url) { 2684 Optional<TermValueSet> optionalExistingTermValueSetByUrl; 2685 if (version != null) { 2686 optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndVersion(url, version); 2687 } else { 2688 optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndNullVersion(url); 2689 } 2690 return optionalExistingTermValueSetByUrl; 2691 } 2692 2693 @Override 2694 @Transactional 2695 public IFhirResourceDaoCodeSystem.SubsumesResult subsumes( 2696 IPrimitiveType<String> theCodeA, 2697 IPrimitiveType<String> theCodeB, 2698 IPrimitiveType<String> theSystem, 2699 IBaseCoding theCodingA, 2700 IBaseCoding theCodingB) { 2701 FhirVersionIndependentConcept conceptA = toConcept(theCodeA, theSystem, theCodingA); 2702 FhirVersionIndependentConcept conceptB = toConcept(theCodeB, theSystem, theCodingB); 2703 2704 if (!StringUtils.equals(conceptA.getSystem(), conceptB.getSystem())) { 2705 throw new InvalidRequestException( 2706 Msg.code(903) + "Unable to test subsumption across different code systems"); 2707 } 2708 2709 if (!StringUtils.equals(conceptA.getSystemVersion(), conceptB.getSystemVersion())) { 2710 throw new InvalidRequestException( 2711 Msg.code(904) + "Unable to test subsumption across different code system versions"); 2712 } 2713 2714 String codeASystemIdentifier; 2715 if (StringUtils.isNotEmpty(conceptA.getSystemVersion())) { 2716 codeASystemIdentifier = conceptA.getSystem() + OUR_PIPE_CHARACTER + conceptA.getSystemVersion(); 2717 } else { 2718 codeASystemIdentifier = conceptA.getSystem(); 2719 } 2720 TermConcept codeA = findCode(codeASystemIdentifier, conceptA.getCode()) 2721 .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptA)); 2722 2723 String codeBSystemIdentifier; 2724 if (StringUtils.isNotEmpty(conceptB.getSystemVersion())) { 2725 codeBSystemIdentifier = conceptB.getSystem() + OUR_PIPE_CHARACTER + conceptB.getSystemVersion(); 2726 } else { 2727 codeBSystemIdentifier = conceptB.getSystem(); 2728 } 2729 TermConcept codeB = findCode(codeBSystemIdentifier, conceptB.getCode()) 2730 .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptB)); 2731 2732 SearchSession searchSession = Search.session(myEntityManager); 2733 2734 ConceptSubsumptionOutcome subsumes; 2735 subsumes = testForSubsumption(searchSession, codeA, codeB, ConceptSubsumptionOutcome.SUBSUMES); 2736 if (subsumes == null) { 2737 subsumes = testForSubsumption(searchSession, codeB, codeA, ConceptSubsumptionOutcome.SUBSUMEDBY); 2738 } 2739 if (subsumes == null) { 2740 subsumes = ConceptSubsumptionOutcome.NOTSUBSUMED; 2741 } 2742 2743 return new IFhirResourceDaoCodeSystem.SubsumesResult(subsumes); 2744 } 2745 2746 @Override 2747 public IValidationSupport.LookupCodeResult lookupCode( 2748 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 2749 TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); 2750 return txTemplate.execute(t -> { 2751 final String theSystem = theLookupCodeRequest.getSystem(); 2752 final String theCode = theLookupCodeRequest.getCode(); 2753 Optional<TermConcept> codeOpt = findCode(theSystem, theCode); 2754 if (codeOpt.isPresent()) { 2755 TermConcept code = codeOpt.get(); 2756 2757 IValidationSupport.LookupCodeResult result = new IValidationSupport.LookupCodeResult(); 2758 result.setCodeSystemDisplayName(code.getCodeSystemVersion().getCodeSystemDisplayName()); 2759 result.setCodeSystemVersion(code.getCodeSystemVersion().getCodeSystemVersionId()); 2760 result.setSearchedForSystem(theSystem); 2761 result.setSearchedForCode(theCode); 2762 result.setFound(true); 2763 result.setCodeDisplay(code.getDisplay()); 2764 2765 for (TermConceptDesignation next : code.getDesignations()) { 2766 // filter out the designation based on displayLanguage if any 2767 if (isDisplayLanguageMatch(theLookupCodeRequest.getDisplayLanguage(), next.getLanguage())) { 2768 IValidationSupport.ConceptDesignation designation = new IValidationSupport.ConceptDesignation(); 2769 designation.setLanguage(next.getLanguage()); 2770 designation.setUseSystem(next.getUseSystem()); 2771 designation.setUseCode(next.getUseCode()); 2772 designation.setUseDisplay(next.getUseDisplay()); 2773 designation.setValue(next.getValue()); 2774 result.getDesignations().add(designation); 2775 } 2776 } 2777 2778 final Collection<String> propertyNames = theLookupCodeRequest.getPropertyNames(); 2779 for (TermConceptProperty next : code.getProperties()) { 2780 if (ObjectUtils.isNotEmpty(propertyNames) && !propertyNames.contains(next.getKey())) { 2781 continue; 2782 } 2783 if (next.getType() == TermConceptPropertyTypeEnum.CODING) { 2784 IValidationSupport.CodingConceptProperty property = 2785 new IValidationSupport.CodingConceptProperty( 2786 next.getKey(), next.getCodeSystem(), next.getValue(), next.getDisplay()); 2787 result.getProperties().add(property); 2788 } else if (next.getType() == TermConceptPropertyTypeEnum.STRING) { 2789 IValidationSupport.StringConceptProperty property = 2790 new IValidationSupport.StringConceptProperty(next.getKey(), next.getValue()); 2791 result.getProperties().add(property); 2792 } else { 2793 throw new InternalErrorException(Msg.code(905) + "Unknown type: " + next.getType()); 2794 } 2795 } 2796 2797 return result; 2798 2799 } else { 2800 return new LookupCodeResult().setFound(false); 2801 } 2802 }); 2803 } 2804 2805 @Nullable 2806 private ConceptSubsumptionOutcome testForSubsumption( 2807 SearchSession theSearchSession, 2808 TermConcept theLeft, 2809 TermConcept theRight, 2810 ConceptSubsumptionOutcome theOutput) { 2811 List<TermConcept> fetch = theSearchSession 2812 .search(TermConcept.class) 2813 .where(f -> f.bool() 2814 .must(f.match().field("myId").matching(theRight.getPid())) 2815 .must(f.match().field("myParentPids").matching(Long.toString(theLeft.getId())))) 2816 .fetchHits(1); 2817 2818 if (fetch.size() > 0) { 2819 return theOutput; 2820 } else { 2821 return null; 2822 } 2823 } 2824 2825 private ArrayList<FhirVersionIndependentConcept> toVersionIndependentConcepts( 2826 String theSystem, Set<TermConcept> codes) { 2827 ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(codes.size()); 2828 for (TermConcept next : codes) { 2829 retVal.add(new FhirVersionIndependentConcept(theSystem, next.getCode())); 2830 } 2831 return retVal; 2832 } 2833 2834 @Override 2835 @Transactional 2836 public CodeValidationResult validateCodeInValueSet( 2837 ValidationSupportContext theValidationSupportContext, 2838 ConceptValidationOptions theOptions, 2839 String theCodeSystem, 2840 String theCode, 2841 String theDisplay, 2842 @Nonnull IBaseResource theValueSet) { 2843 invokeRunnableForUnitTest(); 2844 2845 IPrimitiveType<?> urlPrimitive; 2846 if (theValueSet instanceof org.hl7.fhir.dstu2.model.ValueSet) { 2847 urlPrimitive = FhirContext.forDstu2Hl7OrgCached() 2848 .newTerser() 2849 .getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class); 2850 } else { 2851 urlPrimitive = myContext.newTerser().getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class); 2852 } 2853 String url = urlPrimitive.getValueAsString(); 2854 if (isNotBlank(url)) { 2855 return validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, url); 2856 } 2857 return null; 2858 } 2859 2860 @CoverageIgnore 2861 @Override 2862 public IValidationSupport.CodeValidationResult validateCode( 2863 @Nonnull ValidationSupportContext theValidationSupportContext, 2864 @Nonnull ConceptValidationOptions theOptions, 2865 String theCodeSystemUrl, 2866 String theCode, 2867 String theDisplay, 2868 String theValueSetUrl) { 2869 // TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS. 2870 invokeRunnableForUnitTest(); 2871 theOptions.setValidateDisplay(isNotBlank(theDisplay)); 2872 2873 if (isNotBlank(theValueSetUrl)) { 2874 return validateCodeInValueSet( 2875 theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystemUrl, theCode, theDisplay); 2876 } 2877 2878 TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); 2879 txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 2880 txTemplate.setReadOnly(true); 2881 Optional<FhirVersionIndependentConcept> codeOpt = 2882 txTemplate.execute(tx -> findCode(theCodeSystemUrl, theCode).map(c -> { 2883 String codeSystemVersionId = getCurrentCodeSystemVersion( 2884 theValidationSupportContext, theCodeSystemUrl) 2885 .myCodeSystemVersionId; 2886 return new FhirVersionIndependentConcept( 2887 theCodeSystemUrl, c.getCode(), c.getDisplay(), codeSystemVersionId); 2888 })); 2889 2890 if (codeOpt != null && codeOpt.isPresent()) { 2891 FhirVersionIndependentConcept code = codeOpt.get(); 2892 if (!theOptions.isValidateDisplay() 2893 || isBlank(code.getDisplay()) 2894 || isBlank(theDisplay) 2895 || code.getDisplay().equals(theDisplay)) { 2896 return new CodeValidationResult().setCode(code.getCode()).setDisplay(code.getDisplay()); 2897 } else { 2898 return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch( 2899 myContext, 2900 theCode, 2901 theDisplay, 2902 code.getDisplay(), 2903 code.getSystem(), 2904 code.getSystemVersion(), 2905 myStorageSettings.getIssueSeverityForCodeDisplayMismatch()); 2906 } 2907 } 2908 2909 return createCodeNotFoundErrorForValidationResult( 2910 theCodeSystemUrl, theCode, null, createMessageAppendForCodeNotFoundInCodeSystem(theCodeSystemUrl)); 2911 } 2912 2913 IValidationSupport.CodeValidationResult validateCodeInValueSet( 2914 ValidationSupportContext theValidationSupportContext, 2915 ConceptValidationOptions theValidationOptions, 2916 String theValueSetUrl, 2917 String theCodeSystem, 2918 String theCode, 2919 String theDisplay) { 2920 IBaseResource valueSet = 2921 theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl); 2922 CodeValidationResult retVal = null; 2923 2924 // If we don't have a PID, this came from some source other than the JPA 2925 // database, so we don't need to check if it's pre-expanded or not 2926 if (valueSet instanceof IAnyResource) { 2927 JpaPid pid = IDao.RESOURCE_PID.get(valueSet); 2928 if (pid != null) { 2929 TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); 2930 retVal = txTemplate.execute(tx -> { 2931 if (isValueSetPreExpandedForCodeValidation(valueSet)) { 2932 return validateCodeIsInPreExpandedValueSet( 2933 theValidationSupportContext, 2934 theValidationOptions, 2935 valueSet, 2936 theCodeSystem, 2937 theCode, 2938 theDisplay, 2939 null, 2940 null); 2941 } else { 2942 return null; 2943 } 2944 }); 2945 } 2946 } 2947 2948 if (retVal == null) { 2949 if (valueSet != null) { 2950 retVal = myInMemoryTerminologyServerValidationSupport.validateCodeInValueSet( 2951 theValidationSupportContext, 2952 theValidationOptions, 2953 theCodeSystem, 2954 theCode, 2955 theDisplay, 2956 valueSet); 2957 } else { 2958 String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]"; 2959 retVal = createCodeNotFoundErrorForValidationResult(theCodeSystem, theCode, null, append); 2960 } 2961 } 2962 2963 // Check if someone is accidentally using a VS url where it should be a CS URL 2964 if (retVal != null 2965 && retVal.getCode() == null 2966 && theCodeSystem != null 2967 && myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { 2968 if (isValueSetSupported(theValidationSupportContext, theCodeSystem)) { 2969 if (!isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) { 2970 String newMessage = "Unable to validate code " + theCodeSystem + "#" + theCode 2971 + " - Supplied system URL is a ValueSet URL and not a CodeSystem URL, check if it is correct: " 2972 + theCodeSystem; 2973 retVal.setMessage(newMessage); 2974 } 2975 } 2976 } 2977 2978 return retVal; 2979 } 2980 2981 @Override 2982 public CodeSystem fetchCanonicalCodeSystemFromCompleteContext(String theSystem) { 2983 IValidationSupport validationSupport = provideValidationSupport(); 2984 IBaseResource codeSystem = validationSupport.fetchCodeSystem(theSystem); 2985 if (codeSystem != null) { 2986 codeSystem = myVersionCanonicalizer.codeSystemToCanonical(codeSystem); 2987 } 2988 return (CodeSystem) codeSystem; 2989 } 2990 2991 @Nonnull 2992 private IValidationSupport provideJpaValidationSupport() { 2993 IValidationSupport jpaValidationSupport = myJpaValidationSupport; 2994 if (jpaValidationSupport == null) { 2995 jpaValidationSupport = myApplicationContext.getBean("myJpaValidationSupport", IValidationSupport.class); 2996 myJpaValidationSupport = jpaValidationSupport; 2997 } 2998 return jpaValidationSupport; 2999 } 3000 3001 @Nonnull 3002 protected IValidationSupport provideValidationSupport() { 3003 IValidationSupport validationSupport = myValidationSupport; 3004 if (validationSupport == null) { 3005 validationSupport = myApplicationContext.getBean(IValidationSupport.class); 3006 myValidationSupport = validationSupport; 3007 } 3008 return validationSupport; 3009 } 3010 3011 public ValueSet fetchCanonicalValueSetFromCompleteContext(String theSystem) { 3012 IValidationSupport validationSupport = provideValidationSupport(); 3013 IBaseResource valueSet = validationSupport.fetchValueSet(theSystem); 3014 if (valueSet != null) { 3015 valueSet = myVersionCanonicalizer.valueSetToCanonical(valueSet); 3016 } 3017 return (ValueSet) valueSet; 3018 } 3019 3020 @Override 3021 public IBaseResource fetchValueSet(String theValueSetUrl) { 3022 return provideJpaValidationSupport().fetchValueSet(theValueSetUrl); 3023 } 3024 3025 @Override 3026 public FhirContext getFhirContext() { 3027 return myContext; 3028 } 3029 3030 private void findCodesAbove( 3031 CodeSystem theSystem, 3032 String theSystemString, 3033 String theCode, 3034 List<FhirVersionIndependentConcept> theListToPopulate) { 3035 List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept(); 3036 for (CodeSystem.ConceptDefinitionComponent next : conceptList) { 3037 addTreeIfItContainsCode(theSystemString, next, theCode, theListToPopulate); 3038 } 3039 } 3040 3041 @Override 3042 public List<FhirVersionIndependentConcept> findCodesAboveUsingBuiltInSystems(String theSystem, String theCode) { 3043 ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(); 3044 CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem); 3045 if (system != null) { 3046 findCodesAbove(system, theSystem, theCode, retVal); 3047 } 3048 return retVal; 3049 } 3050 3051 private void findCodesBelow( 3052 CodeSystem theSystem, 3053 String theSystemString, 3054 String theCode, 3055 List<FhirVersionIndependentConcept> theListToPopulate) { 3056 List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept(); 3057 findCodesBelow(theSystemString, theCode, theListToPopulate, conceptList); 3058 } 3059 3060 private void findCodesBelow( 3061 String theSystemString, 3062 String theCode, 3063 List<FhirVersionIndependentConcept> theListToPopulate, 3064 List<CodeSystem.ConceptDefinitionComponent> conceptList) { 3065 for (CodeSystem.ConceptDefinitionComponent next : conceptList) { 3066 if (theCode.equals(next.getCode())) { 3067 addAllChildren(theSystemString, next, theListToPopulate); 3068 } else { 3069 findCodesBelow(theSystemString, theCode, theListToPopulate, next.getConcept()); 3070 } 3071 } 3072 } 3073 3074 @Override 3075 public List<FhirVersionIndependentConcept> findCodesBelowUsingBuiltInSystems(String theSystem, String theCode) { 3076 ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(); 3077 CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem); 3078 if (system != null) { 3079 findCodesBelow(system, theSystem, theCode, retVal); 3080 } 3081 return retVal; 3082 } 3083 3084 private void addAllChildren( 3085 String theSystemString, 3086 CodeSystem.ConceptDefinitionComponent theCode, 3087 List<FhirVersionIndependentConcept> theListToPopulate) { 3088 if (isNotBlank(theCode.getCode())) { 3089 theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theCode.getCode())); 3090 } 3091 for (CodeSystem.ConceptDefinitionComponent nextChild : theCode.getConcept()) { 3092 addAllChildren(theSystemString, nextChild, theListToPopulate); 3093 } 3094 } 3095 3096 private boolean addTreeIfItContainsCode( 3097 String theSystemString, 3098 CodeSystem.ConceptDefinitionComponent theNext, 3099 String theCode, 3100 List<FhirVersionIndependentConcept> theListToPopulate) { 3101 boolean foundCodeInChild = false; 3102 for (CodeSystem.ConceptDefinitionComponent nextChild : theNext.getConcept()) { 3103 foundCodeInChild |= addTreeIfItContainsCode(theSystemString, nextChild, theCode, theListToPopulate); 3104 } 3105 3106 if (theCode.equals(theNext.getCode()) || foundCodeInChild) { 3107 theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theNext.getCode())); 3108 return true; 3109 } 3110 3111 return false; 3112 } 3113 3114 @Nonnull 3115 private FhirVersionIndependentConcept toConcept( 3116 IPrimitiveType<String> theCodeType, 3117 IPrimitiveType<String> theCodeSystemIdentifierType, 3118 IBaseCoding theCodingType) { 3119 String code = theCodeType != null ? theCodeType.getValueAsString() : null; 3120 String system = theCodeSystemIdentifierType != null 3121 ? getUrlFromIdentifier(theCodeSystemIdentifierType.getValueAsString()) 3122 : null; 3123 String systemVersion = theCodeSystemIdentifierType != null 3124 ? getVersionFromIdentifier(theCodeSystemIdentifierType.getValueAsString()) 3125 : null; 3126 if (theCodingType != null) { 3127 Coding canonicalizedCoding = myVersionCanonicalizer.codingToCanonical(theCodingType); 3128 assert canonicalizedCoding != null; // Shouldn't be null, since theCodingType isn't 3129 code = canonicalizedCoding.getCode(); 3130 system = canonicalizedCoding.getSystem(); 3131 systemVersion = canonicalizedCoding.getVersion(); 3132 } 3133 return new FhirVersionIndependentConcept(system, code, null, systemVersion); 3134 } 3135 3136 /** 3137 * When the search is for unversioned loinc system it uses the forcedId to obtain the current 3138 * version, as it is not necessarily the last one anymore. 3139 * For other cases it keeps on considering the last uploaded as the current 3140 */ 3141 @Override 3142 public Optional<TermValueSet> findCurrentTermValueSet(String theUrl) { 3143 if (TermReadSvcUtil.isLoincUnversionedValueSet(theUrl)) { 3144 Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl); 3145 if (vsIdOpt.isEmpty()) { 3146 return Optional.empty(); 3147 } 3148 3149 return myTermValueSetDao.findTermValueSetByForcedId(vsIdOpt.get()); 3150 } 3151 3152 List<TermValueSet> termValueSetList = myTermValueSetDao.findTermValueSetByUrl(Pageable.ofSize(1), theUrl); 3153 if (termValueSetList.isEmpty()) { 3154 return Optional.empty(); 3155 } 3156 3157 return Optional.of(termValueSetList.get(0)); 3158 } 3159 3160 @Override 3161 public Optional<IBaseResource> readCodeSystemByForcedId(String theForcedId) { 3162 @SuppressWarnings("unchecked") 3163 List<ResourceTable> resultList = (List<ResourceTable>) myEntityManager 3164 .createQuery("select r from ResourceTable r " 3165 + "where r.myResourceType = 'CodeSystem' and r.myFhirId = :fhirId") 3166 .setParameter("fhirId", theForcedId) 3167 .getResultList(); 3168 if (resultList.isEmpty()) return Optional.empty(); 3169 3170 if (resultList.size() > 1) 3171 throw new NonUniqueResultException(Msg.code(911) + "More than one CodeSystem is pointed by forcedId: " 3172 + theForcedId + ". Was constraint " + ResourceTable.IDX_RES_TYPE_FHIR_ID + " removed?"); 3173 3174 IFhirResourceDao<CodeSystem> csDao = myDaoRegistry.getResourceDao("CodeSystem"); 3175 IBaseResource cs = myJpaStorageResourceParser.toResource(resultList.get(0), false); 3176 return Optional.of(cs); 3177 } 3178 3179 @Transactional 3180 @Override 3181 public ReindexTerminologyResult reindexTerminology() throws InterruptedException { 3182 if (myFulltextSearchSvc == null) { 3183 return ReindexTerminologyResult.SEARCH_SVC_DISABLED; 3184 } 3185 3186 if (isBatchTerminologyTasksRunning()) { 3187 return ReindexTerminologyResult.OTHER_BATCH_TERMINOLOGY_TASKS_RUNNING; 3188 } 3189 3190 // disallow pre-expanding ValueSets while reindexing 3191 myDeferredStorageSvc.setProcessDeferred(false); 3192 3193 int objectLoadingThreadNumber = calculateObjectLoadingThreadNumber(); 3194 ourLog.info("Using {} threads to load objects", objectLoadingThreadNumber); 3195 3196 try { 3197 SearchSession searchSession = getSearchSession(); 3198 searchSession 3199 .massIndexer(TermConcept.class) 3200 .dropAndCreateSchemaOnStart(true) 3201 .purgeAllOnStart(false) 3202 .batchSizeToLoadObjects(100) 3203 .cacheMode(CacheMode.IGNORE) 3204 .threadsToLoadObjects(6) 3205 .transactionTimeout(60 * SECONDS_IN_MINUTE) 3206 .monitor(new PojoMassIndexingLoggingMonitor(INDEXED_ROOTS_LOGGING_COUNT)) 3207 .startAndWait(); 3208 } finally { 3209 myDeferredStorageSvc.setProcessDeferred(true); 3210 } 3211 3212 return ReindexTerminologyResult.SUCCESS; 3213 } 3214 3215 @VisibleForTesting 3216 boolean isBatchTerminologyTasksRunning() { 3217 return isNotSafeToPreExpandValueSets() || isPreExpandingValueSets(); 3218 } 3219 3220 @VisibleForTesting 3221 int calculateObjectLoadingThreadNumber() { 3222 IConnectionPoolInfoProvider connectionPoolInfoProvider = 3223 new ConnectionPoolInfoProvider(myHibernatePropertiesProvider.getDataSource()); 3224 Optional<Integer> maxConnectionsOpt = connectionPoolInfoProvider.getTotalConnectionSize(); 3225 if (maxConnectionsOpt.isEmpty()) { 3226 return DEFAULT_MASS_INDEXER_OBJECT_LOADING_THREADS; 3227 } 3228 3229 int maxConnections = maxConnectionsOpt.get(); 3230 int usableThreads = maxConnections < 6 ? 1 : maxConnections - 5; 3231 int objectThreads = Math.min(usableThreads, MAX_MASS_INDEXER_OBJECT_LOADING_THREADS); 3232 ourLog.debug( 3233 "Data source connection pool has {} connections allocated, so reindexing will use {} object " 3234 + "loading threads (each using a connection)", 3235 maxConnections, 3236 objectThreads); 3237 return objectThreads; 3238 } 3239 3240 @VisibleForTesting 3241 SearchSession getSearchSession() { 3242 return Search.session(myEntityManager); 3243 } 3244 3245 @Override 3246 public ValueSetExpansionOutcome expandValueSet( 3247 ValidationSupportContext theValidationSupportContext, 3248 ValueSetExpansionOptions theExpansionOptions, 3249 @Nonnull IBaseResource theValueSetToExpand) { 3250 ValueSet canonicalInput = myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand); 3251 org.hl7.fhir.r4.model.ValueSet expandedR4 = expandValueSet(theExpansionOptions, canonicalInput); 3252 return new ValueSetExpansionOutcome(myVersionCanonicalizer.valueSetFromCanonical(expandedR4)); 3253 } 3254 3255 @Override 3256 public IBaseResource expandValueSet(ValueSetExpansionOptions theExpansionOptions, IBaseResource theInput) { 3257 org.hl7.fhir.r4.model.ValueSet valueSetToExpand = myVersionCanonicalizer.valueSetToCanonical(theInput); 3258 org.hl7.fhir.r4.model.ValueSet valueSetR4 = expandValueSet(theExpansionOptions, valueSetToExpand); 3259 return myVersionCanonicalizer.valueSetFromCanonical(valueSetR4); 3260 } 3261 3262 @Override 3263 public void expandValueSet( 3264 ValueSetExpansionOptions theExpansionOptions, 3265 IBaseResource theValueSetToExpand, 3266 IValueSetConceptAccumulator theValueSetCodeAccumulator) { 3267 org.hl7.fhir.r4.model.ValueSet valueSetToExpand = 3268 myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand); 3269 expandValueSet(theExpansionOptions, valueSetToExpand, theValueSetCodeAccumulator); 3270 } 3271 3272 private org.hl7.fhir.r4.model.ValueSet getValueSetFromResourceTable(ResourceTable theResourceTable) { 3273 Class<? extends IBaseResource> type = 3274 getFhirContext().getResourceDefinition("ValueSet").getImplementingClass(); 3275 IBaseResource valueSet = myJpaStorageResourceParser.toResource(type, theResourceTable, null, false); 3276 return myVersionCanonicalizer.valueSetToCanonical(valueSet); 3277 } 3278 3279 @Override 3280 public CodeValidationResult validateCodeIsInPreExpandedValueSet( 3281 ValidationSupportContext theValidationSupportContext, 3282 ConceptValidationOptions theOptions, 3283 IBaseResource theValueSet, 3284 String theSystem, 3285 String theCode, 3286 String theDisplay, 3287 IBaseDatatype theCoding, 3288 IBaseDatatype theCodeableConcept) { 3289 ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null"); 3290 org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet); 3291 org.hl7.fhir.r4.model.Coding codingR4 = myVersionCanonicalizer.codingToCanonical((IBaseCoding) theCoding); 3292 org.hl7.fhir.r4.model.CodeableConcept codeableConcept = 3293 myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept); 3294 3295 return validateCodeIsInPreExpandedValueSet( 3296 theValidationSupportContext, 3297 theOptions, 3298 valueSetR4, 3299 theSystem, 3300 theCode, 3301 theDisplay, 3302 codingR4, 3303 codeableConcept); 3304 } 3305 3306 @Override 3307 public boolean isValueSetPreExpandedForCodeValidation(IBaseResource theValueSet) { 3308 ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null"); 3309 org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet); 3310 return isValueSetPreExpandedForCodeValidation(valueSetR4); 3311 } 3312 3313 private static class TermCodeSystemVersionDetails { 3314 3315 private final long myPid; 3316 private final String myCodeSystemVersionId; 3317 3318 public TermCodeSystemVersionDetails(long thePid, String theCodeSystemVersionId) { 3319 myPid = thePid; 3320 myCodeSystemVersionId = theCodeSystemVersionId; 3321 } 3322 } 3323 3324 public static class Job implements HapiJob { 3325 @Autowired 3326 private ITermReadSvc myTerminologySvc; 3327 3328 @Override 3329 public void execute(JobExecutionContext theContext) { 3330 myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables(); 3331 } 3332 } 3333 3334 /** 3335 * Properties returned from method buildSearchScroll 3336 */ 3337 private static final class SearchProperties { 3338 private final List<Supplier<SearchScroll<EntityReference>>> mySearchScroll = new ArrayList<>(); 3339 private List<String> myIncludeOrExcludeCodes; 3340 3341 public List<Supplier<SearchScroll<EntityReference>>> getSearchScroll() { 3342 return mySearchScroll; 3343 } 3344 3345 public void addSearchScroll(Supplier<SearchScroll<EntityReference>> theSearchScrollSupplier) { 3346 mySearchScroll.add(theSearchScrollSupplier); 3347 } 3348 3349 public List<String> getIncludeOrExcludeCodes() { 3350 return myIncludeOrExcludeCodes; 3351 } 3352 3353 public void setIncludeOrExcludeCodes(List<String> theIncludeOrExcludeCodes) { 3354 myIncludeOrExcludeCodes = theIncludeOrExcludeCodes; 3355 } 3356 3357 public boolean hasIncludeOrExcludeCodes() { 3358 return !myIncludeOrExcludeCodes.isEmpty(); 3359 } 3360 } 3361 3362 static boolean isValueSetDisplayLanguageMatch(ValueSetExpansionOptions theExpansionOptions, String theStoredLang) { 3363 if (theExpansionOptions == null) { 3364 return true; 3365 } 3366 3367 if (theExpansionOptions.getTheDisplayLanguage() == null || theStoredLang == null) { 3368 return true; 3369 } 3370 3371 return theExpansionOptions.getTheDisplayLanguage().equalsIgnoreCase(theStoredLang); 3372 } 3373 3374 @Nonnull 3375 private static String createMessageAppendForCodeNotFoundInCodeSystem(String theCodeSystemUrl) { 3376 return " - Code is not found in CodeSystem: " + theCodeSystemUrl; 3377 } 3378 3379 @VisibleForTesting 3380 public static void setForceDisableHibernateSearchForUnitTest(boolean theForceDisableHibernateSearchForUnitTest) { 3381 ourForceDisableHibernateSearchForUnitTest = theForceDisableHibernateSearchForUnitTest; 3382 } 3383 3384 static boolean isPlaceholder(DomainResource theResource) { 3385 boolean retVal = false; 3386 Extension extension = theResource.getExtensionByUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER); 3387 if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) { 3388 retVal = ((BooleanType) extension.getValue()).booleanValue(); 3389 } 3390 return retVal; 3391 } 3392 3393 /** 3394 * This is only used for unit tests to test failure conditions 3395 */ 3396 static void invokeRunnableForUnitTest() { 3397 if (myInvokeOnNextCallForUnitTest != null) { 3398 Runnable invokeOnNextCallForUnitTest = myInvokeOnNextCallForUnitTest; 3399 myInvokeOnNextCallForUnitTest = null; 3400 invokeOnNextCallForUnitTest.run(); 3401 } 3402 } 3403 3404 @VisibleForTesting 3405 public static void setInvokeOnNextCallForUnitTest(Runnable theInvokeOnNextCallForUnitTest) { 3406 myInvokeOnNextCallForUnitTest = theInvokeOnNextCallForUnitTest; 3407 } 3408 3409 static List<TermConcept> toPersistedConcepts( 3410 List<CodeSystem.ConceptDefinitionComponent> theConcept, TermCodeSystemVersion theCodeSystemVersion) { 3411 ArrayList<TermConcept> retVal = new ArrayList<>(); 3412 3413 for (CodeSystem.ConceptDefinitionComponent next : theConcept) { 3414 if (isNotBlank(next.getCode())) { 3415 TermConcept termConcept = toTermConcept(next, theCodeSystemVersion); 3416 retVal.add(termConcept); 3417 } 3418 } 3419 3420 return retVal; 3421 } 3422 3423 @Nonnull 3424 static TermConcept toTermConcept( 3425 CodeSystem.ConceptDefinitionComponent theConceptDefinition, TermCodeSystemVersion theCodeSystemVersion) { 3426 TermConcept termConcept = new TermConcept(); 3427 termConcept.setCode(theConceptDefinition.getCode()); 3428 termConcept.setCodeSystemVersion(theCodeSystemVersion); 3429 termConcept.setDisplay(theConceptDefinition.getDisplay()); 3430 3431 termConcept.addChildren( 3432 toPersistedConcepts(theConceptDefinition.getConcept(), theCodeSystemVersion), RelationshipTypeEnum.ISA); 3433 3434 for (CodeSystem.ConceptDefinitionDesignationComponent designationComponent : 3435 theConceptDefinition.getDesignation()) { 3436 if (isNotBlank(designationComponent.getValue())) { 3437 TermConceptDesignation designation = termConcept.addDesignation(); 3438 designation.setLanguage(designationComponent.hasLanguage() ? designationComponent.getLanguage() : null); 3439 if (designationComponent.hasUse()) { 3440 designation.setUseSystem( 3441 designationComponent.getUse().hasSystem() 3442 ? designationComponent.getUse().getSystem() 3443 : null); 3444 designation.setUseCode( 3445 designationComponent.getUse().hasCode() 3446 ? designationComponent.getUse().getCode() 3447 : null); 3448 designation.setUseDisplay( 3449 designationComponent.getUse().hasDisplay() 3450 ? designationComponent.getUse().getDisplay() 3451 : null); 3452 } 3453 designation.setValue(designationComponent.getValue()); 3454 } 3455 } 3456 3457 for (CodeSystem.ConceptPropertyComponent next : theConceptDefinition.getProperty()) { 3458 TermConceptProperty property = new TermConceptProperty(); 3459 3460 property.setKey(next.getCode()); 3461 property.setConcept(termConcept); 3462 property.setCodeSystemVersion(theCodeSystemVersion); 3463 3464 if (next.getValue() instanceof StringType) { 3465 property.setType(TermConceptPropertyTypeEnum.STRING); 3466 property.setValue(next.getValueStringType().getValue()); 3467 } else if (next.getValue() instanceof BooleanType) { 3468 property.setType(TermConceptPropertyTypeEnum.BOOLEAN); 3469 property.setValue(((BooleanType) next.getValue()).getValueAsString()); 3470 } else if (next.getValue() instanceof IntegerType) { 3471 property.setType(TermConceptPropertyTypeEnum.INTEGER); 3472 property.setValue(((IntegerType) next.getValue()).getValueAsString()); 3473 } else if (next.getValue() instanceof DecimalType) { 3474 property.setType(TermConceptPropertyTypeEnum.DECIMAL); 3475 property.setValue(((DecimalType) next.getValue()).getValueAsString()); 3476 } else if (next.getValue() instanceof DateTimeType) { 3477 // DateType is not supported because it's not 3478 // supported in CodeSystem.setValue 3479 property.setType(TermConceptPropertyTypeEnum.DATETIME); 3480 property.setValue(((DateTimeType) next.getValue()).getValueAsString()); 3481 } else if (next.getValue() instanceof Coding) { 3482 Coding nextCoding = next.getValueCoding(); 3483 property.setType(TermConceptPropertyTypeEnum.CODING); 3484 property.setCodeSystem(nextCoding.getSystem()); 3485 property.setValue(nextCoding.getCode()); 3486 property.setDisplay(nextCoding.getDisplay()); 3487 } else if (next.getValue() != null) { 3488 ourLog.warn("Don't know how to handle properties of type: " 3489 + next.getValue().getClass()); 3490 continue; 3491 } 3492 3493 termConcept.getProperties().add(property); 3494 } 3495 return termConcept; 3496 } 3497 3498 static boolean isDisplayLanguageMatch(String theReqLang, String theStoredLang) { 3499 // NOTE: return the designation when one of then is not specified. 3500 if (theReqLang == null || theStoredLang == null) return true; 3501 3502 return theReqLang.equalsIgnoreCase(theStoredLang); 3503 } 3504}