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