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