001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.reindex; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.interceptor.api.IInterceptorService; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 027import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 028import ca.uhn.fhir.jpa.api.dao.ReindexOutcome; 029import ca.uhn.fhir.jpa.api.dao.ReindexParameters; 030import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; 031import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 032import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 033import ca.uhn.fhir.jpa.model.config.PartitionSettings; 034import ca.uhn.fhir.jpa.model.dao.JpaPid; 035import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 036import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 037import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; 038import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; 039import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 040import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; 041import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; 042import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; 043import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; 044import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 045import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; 046import ca.uhn.fhir.jpa.model.entity.ResourceLink; 047import ca.uhn.fhir.jpa.model.entity.ResourceTable; 048import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity; 049import ca.uhn.fhir.jpa.partition.BaseRequestPartitionHelperSvc; 050import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 051import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 052import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; 053import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; 054import ca.uhn.fhir.rest.api.server.RequestDetails; 055import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 056import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 057import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 058import ca.uhn.fhir.util.StopWatch; 059import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 060import com.google.common.annotations.VisibleForTesting; 061import jakarta.annotation.Nonnull; 062import jakarta.annotation.Nullable; 063import org.hl7.fhir.instance.model.api.IBaseParameters; 064import org.hl7.fhir.instance.model.api.IBaseResource; 065import org.hl7.fhir.instance.model.api.IIdType; 066import org.hl7.fhir.r4.model.BooleanType; 067import org.hl7.fhir.r4.model.CodeType; 068import org.hl7.fhir.r4.model.DecimalType; 069import org.hl7.fhir.r4.model.InstantType; 070import org.hl7.fhir.r4.model.Parameters; 071import org.hl7.fhir.r4.model.StringType; 072import org.hl7.fhir.r4.model.UriType; 073import org.hl7.fhir.r4.model.UrlType; 074import org.springframework.beans.factory.annotation.Autowired; 075 076import java.util.ArrayList; 077import java.util.Collection; 078import java.util.HashMap; 079import java.util.List; 080import java.util.Map; 081import java.util.Set; 082import java.util.stream.Collectors; 083 084import static ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer.subtract; 085import static java.util.Comparator.comparing; 086import static org.apache.commons.collections4.CollectionUtils.intersection; 087import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 088import static org.apache.commons.lang3.StringUtils.isNotBlank; 089 090public class InstanceReindexServiceImpl implements IInstanceReindexService { 091 092 private final FhirContext myContextR4 = FhirContext.forR4Cached(); 093 094 @Autowired 095 protected IJpaStorageResourceParser myJpaStorageResourceParser; 096 097 @Autowired 098 private SearchParamExtractorService mySearchParamExtractorService; 099 100 @Autowired 101 private BaseRequestPartitionHelperSvc myPartitionHelperSvc; 102 103 @Autowired 104 private IHapiTransactionService myTransactionService; 105 106 @Autowired 107 private IInterceptorService myInterceptorService; 108 109 @Autowired 110 private DaoRegistry myDaoRegistry; 111 112 @Autowired 113 private VersionCanonicalizer myVersionCanonicalizer; 114 115 @Autowired 116 private PartitionSettings myPartitionSettings; 117 118 private final CustomThymeleafNarrativeGenerator myNarrativeGenerator; 119 120 @Autowired 121 private ISearchParamRegistry mySearchParamRegistry; 122 123 /** 124 * Constructor 125 */ 126 public InstanceReindexServiceImpl() { 127 myNarrativeGenerator = new CustomThymeleafNarrativeGenerator( 128 "classpath:ca/uhn/fhir/jpa/search/reindex/reindex-outcome-narrative.properties"); 129 } 130 131 @Override 132 public IBaseParameters reindexDryRun( 133 RequestDetails theRequestDetails, IIdType theResourceId, @Nullable Set<String> theParameters) { 134 RequestPartitionId partitionId = determinePartition(theRequestDetails, theResourceId); 135 TransactionDetails transactionDetails = new TransactionDetails(); 136 137 Parameters retValCanonical = myTransactionService 138 .withRequest(theRequestDetails) 139 .withTransactionDetails(transactionDetails) 140 .withRequestPartitionId(partitionId) 141 .execute(() -> reindexDryRunInTransaction( 142 theRequestDetails, theResourceId, partitionId, transactionDetails, theParameters)); 143 144 return myVersionCanonicalizer.parametersFromCanonical(retValCanonical); 145 } 146 147 @Override 148 public IBaseParameters reindex(RequestDetails theRequestDetails, IIdType theResourceId) { 149 RequestPartitionId partitionId = determinePartition(theRequestDetails, theResourceId); 150 TransactionDetails transactionDetails = new TransactionDetails(); 151 152 Parameters retValCanonical = myTransactionService 153 .withRequest(theRequestDetails) 154 .withTransactionDetails(transactionDetails) 155 .withRequestPartitionId(partitionId) 156 .execute(() -> reindexInTransaction(theRequestDetails, theResourceId)); 157 158 return myVersionCanonicalizer.parametersFromCanonical(retValCanonical); 159 } 160 161 @SuppressWarnings({"rawtypes"}) 162 @Nonnull 163 private Parameters reindexInTransaction(RequestDetails theRequestDetails, IIdType theResourceId) { 164 StopWatch sw = new StopWatch(); 165 IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType()); 166 ResourceTable entity = (ResourceTable) dao.readEntity(theResourceId, theRequestDetails); 167 IBaseResource resource = myJpaStorageResourceParser.toResource(entity, false); 168 169 // Invoke the pre-access and pre-show interceptors in case there are any security 170 // restrictions or audit requirements around the user accessing this resource 171 BaseHapiFhirResourceDao.invokeStoragePreAccessResources( 172 myInterceptorService, theRequestDetails, theResourceId, resource); 173 BaseHapiFhirResourceDao.invokeStoragePreShowResources(myInterceptorService, theRequestDetails, resource); 174 175 ResourceIndexedSearchParams existingParamsToPopulate = ResourceIndexedSearchParams.withLists(entity); 176 existingParamsToPopulate.mySearchParamPresentEntities.addAll(entity.getSearchParamPresents()); 177 178 List<String> messages = new ArrayList<>(); 179 180 JpaPid pid = entity.getPersistentId(); 181 ReindexOutcome outcome = dao.reindex(pid, new ReindexParameters(), theRequestDetails, new TransactionDetails()); 182 messages.add("Reindex completed in " + sw); 183 184 for (String next : outcome.getWarnings()) { 185 messages.add("WARNING: " + next); 186 } 187 188 ResourceIndexedSearchParams newParamsToPopulate = ResourceIndexedSearchParams.withLists(entity); 189 newParamsToPopulate.mySearchParamPresentEntities.addAll(entity.getSearchParamPresents()); 190 191 return buildIndexResponse(existingParamsToPopulate, newParamsToPopulate, true, messages); 192 } 193 194 @Nonnull 195 private Parameters reindexDryRunInTransaction( 196 RequestDetails theRequestDetails, 197 IIdType theResourceId, 198 RequestPartitionId theRequestPartitionId, 199 TransactionDetails theTransactionDetails, 200 Set<String> theParameters) { 201 StopWatch sw = new StopWatch(); 202 203 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType()); 204 ResourceTable entity = (ResourceTable) dao.readEntity(theResourceId, theRequestDetails); 205 IBaseResource resource = myJpaStorageResourceParser.toResource(entity, false); 206 207 // Invoke the pre-access and pre-show interceptors in case there are any security 208 // restrictions or audit requirements around the user accessing this resource 209 BaseHapiFhirResourceDao.invokeStoragePreAccessResources( 210 myInterceptorService, theRequestDetails, theResourceId, resource); 211 BaseHapiFhirResourceDao.invokeStoragePreShowResources(myInterceptorService, theRequestDetails, resource); 212 213 ISearchParamExtractor.ISearchParamFilter searchParamFilter = ISearchParamExtractor.ALL_PARAMS; 214 if (theParameters != null) { 215 searchParamFilter = params -> params.stream() 216 .filter(t -> theParameters.contains(t.getName())) 217 .collect(Collectors.toSet()); 218 } 219 220 ResourceIndexedSearchParams newParamsToPopulate = ResourceIndexedSearchParams.withSets(); 221 mySearchParamExtractorService.extractFromResource( 222 theRequestPartitionId, 223 theRequestDetails, 224 newParamsToPopulate, 225 ResourceIndexedSearchParams.empty(), 226 entity, 227 resource, 228 theTransactionDetails, 229 false, 230 searchParamFilter); 231 232 ResourceIndexedSearchParams existingParamsToPopulate; 233 boolean showAction; 234 if (theParameters == null) { 235 existingParamsToPopulate = ResourceIndexedSearchParams.withLists(entity); 236 existingParamsToPopulate.mySearchParamPresentEntities.addAll(entity.getSearchParamPresents()); 237 fillInParamNames( 238 entity, existingParamsToPopulate.mySearchParamPresentEntities, theResourceId.getResourceType()); 239 showAction = true; 240 } else { 241 existingParamsToPopulate = ResourceIndexedSearchParams.withSets(); 242 showAction = false; 243 } 244 245 String message = "Reindex dry-run completed in " + sw + ". No changes were committed to any stored data."; 246 return buildIndexResponse(existingParamsToPopulate, newParamsToPopulate, showAction, List.of(message)); 247 } 248 249 @Nonnull 250 private RequestPartitionId determinePartition(RequestDetails theRequestDetails, IIdType theResourceId) { 251 return myPartitionHelperSvc.determineReadPartitionForRequestForRead(theRequestDetails, theResourceId); 252 } 253 254 @Nonnull 255 @VisibleForTesting 256 Parameters buildIndexResponse( 257 ResourceIndexedSearchParams theExistingParams, 258 ResourceIndexedSearchParams theNewParams, 259 boolean theShowAction, 260 List<String> theMessages) { 261 Parameters parameters = new Parameters(); 262 263 Parameters.ParametersParameterComponent narrativeParameter = parameters.addParameter(); 264 narrativeParameter.setName("Narrative"); 265 266 for (String next : theMessages) { 267 parameters.addParameter("Message", new StringType(next)); 268 } 269 270 // Normal indexes 271 addParamsNonMissing( 272 parameters, 273 "CoordinateIndexes", 274 "Coords", 275 theExistingParams.myCoordsParams, 276 theNewParams.myCoordsParams, 277 new CoordsParamPopulator(), 278 theShowAction); 279 addParamsNonMissing( 280 parameters, 281 "DateIndexes", 282 "Date", 283 theExistingParams.myDateParams, 284 theNewParams.myDateParams, 285 new DateParamPopulator(), 286 theShowAction); 287 addParamsNonMissing( 288 parameters, 289 "NumberIndexes", 290 "Number", 291 theExistingParams.myNumberParams, 292 theNewParams.myNumberParams, 293 new NumberParamPopulator(), 294 theShowAction); 295 addParamsNonMissing( 296 parameters, 297 "QuantityIndexes", 298 "Quantity", 299 theExistingParams.myQuantityParams, 300 theNewParams.myQuantityParams, 301 new QuantityParamPopulator(), 302 theShowAction); 303 addParamsNonMissing( 304 parameters, 305 "QuantityIndexes", 306 "QuantityNormalized", 307 theExistingParams.myQuantityNormalizedParams, 308 theNewParams.myQuantityNormalizedParams, 309 new QuantityNormalizedParamPopulator(), 310 theShowAction); 311 addParamsNonMissing( 312 parameters, 313 "UriIndexes", 314 "Uri", 315 theExistingParams.myUriParams, 316 theNewParams.myUriParams, 317 new UriParamPopulator(), 318 theShowAction); 319 addParamsNonMissing( 320 parameters, 321 "StringIndexes", 322 "String", 323 theExistingParams.myStringParams, 324 theNewParams.myStringParams, 325 new StringParamPopulator(), 326 theShowAction); 327 addParamsNonMissing( 328 parameters, 329 "TokenIndexes", 330 "Token", 331 theExistingParams.myTokenParams, 332 theNewParams.myTokenParams, 333 new TokenParamPopulator(), 334 theShowAction); 335 336 // Resource links 337 addParams( 338 parameters, 339 "ResourceLinks", 340 "Reference", 341 normalizeLinks(theExistingParams.myLinks), 342 normalizeLinks(theNewParams.myLinks), 343 new ResourceLinkPopulator(), 344 theShowAction); 345 346 // Combo search params 347 addParams( 348 parameters, 349 "UniqueIndexes", 350 "ComboStringUnique", 351 theExistingParams.myComboStringUniques, 352 theNewParams.myComboStringUniques, 353 new ComboStringUniquePopulator(), 354 theShowAction); 355 addParams( 356 parameters, 357 "NonUniqueIndexes", 358 "ComboTokenNonUnique", 359 theExistingParams.myComboTokenNonUnique, 360 theNewParams.myComboTokenNonUnique, 361 new ComboTokenNonUniquePopulator(), 362 theShowAction); 363 364 // Missing (:missing) indexes 365 addParamsMissing( 366 parameters, 367 "Coords", 368 theExistingParams.myCoordsParams, 369 theNewParams.myCoordsParams, 370 new MissingIndexParamPopulator<>(), 371 theShowAction); 372 addParamsMissing( 373 parameters, 374 "Date", 375 theExistingParams.myDateParams, 376 theNewParams.myDateParams, 377 new MissingIndexParamPopulator<>(), 378 theShowAction); 379 addParamsMissing( 380 parameters, 381 "Number", 382 theExistingParams.myNumberParams, 383 theNewParams.myNumberParams, 384 new MissingIndexParamPopulator<>(), 385 theShowAction); 386 addParamsMissing( 387 parameters, 388 "Quantity", 389 theExistingParams.myQuantityParams, 390 theNewParams.myQuantityParams, 391 new MissingIndexParamPopulator<>(), 392 theShowAction); 393 addParamsMissing( 394 parameters, 395 "QuantityNormalized", 396 theExistingParams.myQuantityNormalizedParams, 397 theNewParams.myQuantityNormalizedParams, 398 new MissingIndexParamPopulator<>(), 399 theShowAction); 400 addParamsMissing( 401 parameters, 402 "Uri", 403 theExistingParams.myUriParams, 404 theNewParams.myUriParams, 405 new MissingIndexParamPopulator<>(), 406 theShowAction); 407 addParamsMissing( 408 parameters, 409 "String", 410 theExistingParams.myStringParams, 411 theNewParams.myStringParams, 412 new MissingIndexParamPopulator<>(), 413 theShowAction); 414 addParamsMissing( 415 parameters, 416 "Token", 417 theExistingParams.myTokenParams, 418 theNewParams.myTokenParams, 419 new MissingIndexParamPopulator<>(), 420 theShowAction); 421 addParams( 422 parameters, 423 "MissingIndexes", 424 "Reference", 425 theExistingParams.mySearchParamPresentEntities, 426 theNewParams.mySearchParamPresentEntities, 427 new SearchParamPresentParamPopulator(), 428 theShowAction); 429 430 String narrativeText = myNarrativeGenerator.generateResourceNarrative(myContextR4, parameters); 431 narrativeParameter.setValue(new StringType(narrativeText)); 432 433 return parameters; 434 } 435 436 /** 437 * The {@link SearchParamPresentEntity} entity doesn't actually store the parameter names 438 * in the database entity, it only stores a hash. So we brute force possible hashes here 439 * to figure out the associated param names. 440 */ 441 private void fillInParamNames( 442 ResourceTable theEntity, Collection<SearchParamPresentEntity> theTarget, String theResourceName) { 443 Map<Long, String> hashes = new HashMap<>(); 444 ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams( 445 theResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.ALL); 446 for (RuntimeSearchParam next : searchParams.values()) { 447 hashes.put( 448 SearchParamPresentEntity.calculateHashPresence( 449 myPartitionSettings, theEntity.getPartitionId(), theResourceName, next.getName(), true), 450 next.getName()); 451 hashes.put( 452 SearchParamPresentEntity.calculateHashPresence( 453 myPartitionSettings, theEntity.getPartitionId(), theResourceName, next.getName(), false), 454 next.getName()); 455 } 456 457 for (SearchParamPresentEntity next : theTarget) { 458 if (next.getParamName() == null) { 459 String name = hashes.get(next.getHashPresence()); 460 name = defaultIfBlank(name, "(unknown)"); 461 next.setParamName(name); 462 } 463 } 464 } 465 466 private enum ActionEnum { 467 ADD, 468 REMOVE, 469 UNKNOWN, 470 NO_CHANGE 471 } 472 473 private abstract static class BaseParamPopulator<T> { 474 475 @Nonnull 476 public Parameters.ParametersParameterComponent addIndexValue( 477 ActionEnum theAction, 478 Parameters.ParametersParameterComponent theParent, 479 T theParam, 480 String theParamTypeName) { 481 Parameters.ParametersParameterComponent retVal = theParent.addPart().setName(toPartName(theParam)); 482 retVal.addPart().setName("Action").setValue(new CodeType(theAction.name())); 483 if (theParamTypeName != null) { 484 retVal.addPart().setName("Type").setValue(new CodeType(theParamTypeName)); 485 } 486 return retVal; 487 } 488 489 protected abstract String toPartName(T theParam); 490 491 public void sort(List<T> theParams) { 492 theParams.sort(comparing(this::toPartName)); 493 } 494 } 495 496 public abstract static class BaseIndexParamPopulator<T extends BaseResourceIndexedSearchParam> 497 extends BaseParamPopulator<T> { 498 @Override 499 protected String toPartName(T theParam) { 500 return theParam.getParamName(); 501 } 502 } 503 504 private static class ComboStringUniquePopulator extends BaseParamPopulator<ResourceIndexedComboStringUnique> { 505 @Override 506 protected String toPartName(ResourceIndexedComboStringUnique theParam) { 507 return theParam.getIndexString(); 508 } 509 } 510 511 private static class ComboTokenNonUniquePopulator extends BaseParamPopulator<ResourceIndexedComboTokenNonUnique> { 512 @Override 513 protected String toPartName(ResourceIndexedComboTokenNonUnique theParam) { 514 return theParam.getIndexString(); 515 } 516 } 517 518 private static class CoordsParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamCoords> { 519 @Nonnull 520 @Override 521 public Parameters.ParametersParameterComponent addIndexValue( 522 ActionEnum theAction, 523 Parameters.ParametersParameterComponent theParent, 524 ResourceIndexedSearchParamCoords theParam, 525 String theParamTypeName) { 526 Parameters.ParametersParameterComponent retVal = 527 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 528 if (theParam.getLatitude() != null) { 529 retVal.addPart().setName("Latitude").setValue(new DecimalType(theParam.getLatitude())); 530 } 531 if (theParam.getLongitude() != null) { 532 retVal.addPart().setName("Longitude").setValue(new DecimalType(theParam.getLongitude())); 533 } 534 return retVal; 535 } 536 } 537 538 private static class DateParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamDate> { 539 540 @Nonnull 541 @Override 542 public Parameters.ParametersParameterComponent addIndexValue( 543 ActionEnum theAction, 544 Parameters.ParametersParameterComponent theParent, 545 ResourceIndexedSearchParamDate theParam, 546 String theParamTypeName) { 547 Parameters.ParametersParameterComponent retVal = 548 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 549 retVal.addPart().setName("High").setValue(new InstantType(theParam.getValueHigh())); 550 retVal.addPart().setName("Low").setValue(new InstantType(theParam.getValueLow())); 551 return retVal; 552 } 553 } 554 555 private static class MissingIndexParamPopulator<T extends BaseResourceIndexedSearchParam> 556 extends BaseIndexParamPopulator<T> { 557 @Nonnull 558 @Override 559 public Parameters.ParametersParameterComponent addIndexValue( 560 ActionEnum theAction, 561 Parameters.ParametersParameterComponent theParent, 562 T theParam, 563 String theParamTypeName) { 564 Parameters.ParametersParameterComponent retVal = 565 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 566 retVal.addPart().setName("Missing").setValue(new BooleanType(theParam.isMissing())); 567 return retVal; 568 } 569 } 570 571 private static class NumberParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamNumber> { 572 573 @Nonnull 574 @Override 575 public Parameters.ParametersParameterComponent addIndexValue( 576 ActionEnum theAction, 577 Parameters.ParametersParameterComponent theParent, 578 ResourceIndexedSearchParamNumber theParam, 579 String theParamTypeName) { 580 Parameters.ParametersParameterComponent retVal = 581 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 582 retVal.addPart().setName("Value").setValue(new DecimalType(theParam.getValue())); 583 return retVal; 584 } 585 } 586 587 private static class QuantityParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamQuantity> { 588 589 @Nonnull 590 @Override 591 public Parameters.ParametersParameterComponent addIndexValue( 592 ActionEnum theAction, 593 Parameters.ParametersParameterComponent theParent, 594 ResourceIndexedSearchParamQuantity theParam, 595 String theParamTypeName) { 596 Parameters.ParametersParameterComponent retVal = 597 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 598 retVal.addPart().setName("Value").setValue(new DecimalType(theParam.getValue())); 599 retVal.addPart().setName("System").setValue(new UriType(theParam.getSystem())); 600 retVal.addPart().setName("Units").setValue(new CodeType(theParam.getUnits())); 601 return retVal; 602 } 603 } 604 605 private static class QuantityNormalizedParamPopulator 606 extends BaseIndexParamPopulator<ResourceIndexedSearchParamQuantityNormalized> { 607 608 @Nonnull 609 @Override 610 public Parameters.ParametersParameterComponent addIndexValue( 611 ActionEnum theAction, 612 Parameters.ParametersParameterComponent theParent, 613 ResourceIndexedSearchParamQuantityNormalized theParam, 614 String theParamTypeName) { 615 Parameters.ParametersParameterComponent retVal = 616 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 617 retVal.addPart().setName("Value").setValue(new DecimalType(theParam.getValue())); 618 retVal.addPart().setName("System").setValue(new UriType(theParam.getSystem())); 619 retVal.addPart().setName("Units").setValue(new CodeType(theParam.getUnits())); 620 return retVal; 621 } 622 } 623 624 private static class ResourceLinkPopulator extends BaseParamPopulator<ResourceLink> { 625 626 @Nonnull 627 @Override 628 public Parameters.ParametersParameterComponent addIndexValue( 629 ActionEnum theAction, 630 Parameters.ParametersParameterComponent theParent, 631 ResourceLink theParam, 632 String theParamTypeName) { 633 Parameters.ParametersParameterComponent retVal = 634 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 635 if (theParam.getTargetResourceId() != null) { 636 retVal.addPart() 637 .setName("TargetId") 638 .setValue(new StringType( 639 theParam.getTargetResourceType() + "/" + theParam.getTargetResourceId())); 640 } else if (theParam.getTargetResourceUrl() != null) { 641 retVal.addPart().setName("TargetUrl").setValue(new UrlType(theParam.getTargetResourceUrl())); 642 } 643 644 if (theParam.getTargetResourceVersion() != null) { 645 retVal.addPart() 646 .setName("TargetVersion") 647 .setValue(new StringType( 648 theParam.getTargetResourceVersion().toString())); 649 } 650 651 return retVal; 652 } 653 654 @Override 655 protected String toPartName(ResourceLink theParam) { 656 return theParam.getSourcePath(); 657 } 658 } 659 660 private static class SearchParamPresentParamPopulator extends BaseParamPopulator<SearchParamPresentEntity> { 661 @Nonnull 662 @Override 663 public Parameters.ParametersParameterComponent addIndexValue( 664 ActionEnum theAction, 665 Parameters.ParametersParameterComponent theParent, 666 SearchParamPresentEntity theParam, 667 String theParamTypeName) { 668 Parameters.ParametersParameterComponent retVal = 669 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 670 retVal.addPart().setName("Missing").setValue(new BooleanType(!theParam.isPresent())); 671 return retVal; 672 } 673 674 @Override 675 protected String toPartName(SearchParamPresentEntity theParam) { 676 return theParam.getParamName(); 677 } 678 } 679 680 private static class StringParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamString> { 681 682 @Nonnull 683 @Override 684 public Parameters.ParametersParameterComponent addIndexValue( 685 ActionEnum theAction, 686 Parameters.ParametersParameterComponent theParent, 687 ResourceIndexedSearchParamString theParam, 688 String theParamTypeName) { 689 Parameters.ParametersParameterComponent retVal = 690 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 691 retVal.addPart().setName("ValueNormalized").setValue(new StringType(theParam.getValueNormalized())); 692 retVal.addPart().setName("ValueExact").setValue(new StringType(theParam.getValueExact())); 693 return retVal; 694 } 695 } 696 697 private static class TokenParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamToken> { 698 699 @Nonnull 700 @Override 701 public Parameters.ParametersParameterComponent addIndexValue( 702 ActionEnum theAction, 703 Parameters.ParametersParameterComponent theParent, 704 ResourceIndexedSearchParamToken theParam, 705 String theParamTypeName) { 706 Parameters.ParametersParameterComponent retVal = 707 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 708 if (isNotBlank(theParam.getSystem())) { 709 retVal.addPart().setName("System").setValue(new StringType(theParam.getSystem())); 710 } 711 if (isNotBlank(theParam.getValue())) { 712 retVal.addPart().setName("Value").setValue(new StringType(theParam.getValue())); 713 } 714 return retVal; 715 } 716 } 717 718 private static class UriParamPopulator extends BaseIndexParamPopulator<ResourceIndexedSearchParamUri> { 719 720 @Nonnull 721 @Override 722 public Parameters.ParametersParameterComponent addIndexValue( 723 ActionEnum theAction, 724 Parameters.ParametersParameterComponent theParent, 725 ResourceIndexedSearchParamUri theParam, 726 String theParamTypeName) { 727 Parameters.ParametersParameterComponent retVal = 728 super.addIndexValue(theAction, theParent, theParam, theParamTypeName); 729 retVal.addPart().setName("Value").setValue(new UriType(theParam.getUri())); 730 return retVal; 731 } 732 } 733 734 /** 735 * Links loaded from the database have a PID link to their target, but the ones 736 * extracted from the resource in memory won't have the PID. So this method 737 * strips the PIDs so that the generated hashCodes and equals comparisons 738 * will actually be equal. 739 */ 740 private static List<ResourceLink> normalizeLinks(Collection<ResourceLink> theLinks) { 741 return theLinks.stream().map(ResourceLink::cloneWithoutTargetPid).collect(Collectors.toList()); 742 } 743 744 private static <T> void addParams( 745 Parameters theParameters, 746 String theSectionName, 747 String theTypeName, 748 Collection<T> theExistingParams, 749 Collection<T> theNewParams, 750 BaseParamPopulator<T> thePopulator, 751 boolean theShowAction) { 752 List<T> addedParams = subtract(theNewParams, theExistingParams); 753 thePopulator.sort(addedParams); 754 for (T next : addedParams) { 755 Parameters.ParametersParameterComponent parent = getOrCreateSection(theParameters, theSectionName); 756 if (theShowAction) { 757 thePopulator.addIndexValue(ActionEnum.ADD, parent, next, theTypeName); 758 } else { 759 thePopulator.addIndexValue(ActionEnum.UNKNOWN, parent, next, theTypeName); 760 } 761 } 762 763 List<T> removedParams = subtract(theExistingParams, theNewParams); 764 addedParams.sort(comparing(thePopulator::toPartName)); 765 for (T next : removedParams) { 766 Parameters.ParametersParameterComponent parent = getOrCreateSection(theParameters, theSectionName); 767 thePopulator.addIndexValue(ActionEnum.REMOVE, parent, next, theTypeName); 768 } 769 770 List<T> unchangedParams = new ArrayList<>(intersection(theNewParams, theExistingParams)); 771 addedParams.sort(comparing(thePopulator::toPartName)); 772 for (T next : unchangedParams) { 773 Parameters.ParametersParameterComponent parent = getOrCreateSection(theParameters, theSectionName); 774 thePopulator.addIndexValue(ActionEnum.NO_CHANGE, parent, next, theTypeName); 775 } 776 } 777 778 private static <T extends BaseResourceIndexedSearchParam> void addParamsNonMissing( 779 Parameters theParameters, 780 String theSectionName, 781 String theTypeName, 782 Collection<T> theExistingParams, 783 Collection<T> theNewParams, 784 BaseParamPopulator<T> thePopulator, 785 boolean theShowAction) { 786 Collection<T> existingParams = filterWantMissing(theExistingParams, false); 787 Collection<T> newParams = filterWantMissing(theNewParams, false); 788 addParams(theParameters, theSectionName, theTypeName, existingParams, newParams, thePopulator, theShowAction); 789 } 790 791 private static <T extends BaseResourceIndexedSearchParam> void addParamsMissing( 792 Parameters theParameters, 793 String theTypeName, 794 Collection<T> theExistingParams, 795 Collection<T> theNewParams, 796 BaseParamPopulator<T> thePopulator, 797 boolean theShowAction) { 798 Collection<T> existingParams = filterWantMissing(theExistingParams, true); 799 Collection<T> newParams = filterWantMissing(theNewParams, true); 800 addParams(theParameters, "MissingIndexes", theTypeName, existingParams, newParams, thePopulator, theShowAction); 801 } 802 803 private static <T extends BaseResourceIndexedSearchParam> Collection<T> filterWantMissing( 804 Collection<T> theNewParams, boolean theWantMissing) { 805 return theNewParams.stream() 806 .filter(t -> t.isMissing() == theWantMissing) 807 .collect(Collectors.toList()); 808 } 809 810 @Nonnull 811 private static Parameters.ParametersParameterComponent getOrCreateSection( 812 Parameters theParameters, String theSectionName) { 813 Parameters.ParametersParameterComponent parent = theParameters.getParameter(theSectionName); 814 if (parent == null) { 815 parent = theParameters.addParameter(); 816 parent.setName(theSectionName); 817 } 818 return parent; 819 } 820}