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