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