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