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