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