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}