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}