001/*
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
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.searchparam.extractor;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.ComboSearchParamType;
026import ca.uhn.fhir.context.FhirContext;
027import ca.uhn.fhir.context.FhirVersionEnum;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.context.RuntimeSearchParam;
030import ca.uhn.fhir.i18n.Msg;
031import ca.uhn.fhir.jpa.model.config.PartitionSettings;
032import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
033import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParamQuantity;
034import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
035import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
036import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords;
037import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
038import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
039import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
040import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
041import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
042import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
043import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
044import ca.uhn.fhir.jpa.model.entity.ResourceLink;
045import ca.uhn.fhir.jpa.model.entity.StorageSettings;
046import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
047import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
048import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
049import ca.uhn.fhir.jpa.searchparam.util.RuntimeSearchParamHelper;
050import ca.uhn.fhir.model.api.IQueryParameterType;
051import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
052import ca.uhn.fhir.model.primitive.BoundCodeDt;
053import ca.uhn.fhir.rest.api.Constants;
054import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
055import ca.uhn.fhir.rest.param.DateParam;
056import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
057import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
058import ca.uhn.fhir.util.FhirTerser;
059import ca.uhn.fhir.util.HapiExtensions;
060import ca.uhn.fhir.util.StringUtil;
061import ca.uhn.fhir.util.UrlUtil;
062import com.google.common.annotations.VisibleForTesting;
063import com.google.common.collect.Sets;
064import jakarta.annotation.Nonnull;
065import jakarta.annotation.Nullable;
066import jakarta.annotation.PostConstruct;
067import org.apache.commons.lang3.ObjectUtils;
068import org.apache.commons.lang3.StringUtils;
069import org.apache.commons.lang3.Validate;
070import org.apache.commons.text.StringTokenizer;
071import org.fhir.ucum.Pair;
072import org.hl7.fhir.exceptions.FHIRException;
073import org.hl7.fhir.instance.model.api.IBase;
074import org.hl7.fhir.instance.model.api.IBaseEnumeration;
075import org.hl7.fhir.instance.model.api.IBaseExtension;
076import org.hl7.fhir.instance.model.api.IBaseReference;
077import org.hl7.fhir.instance.model.api.IBaseResource;
078import org.hl7.fhir.instance.model.api.IIdType;
079import org.hl7.fhir.instance.model.api.IPrimitiveType;
080import org.springframework.beans.factory.annotation.Autowired;
081import org.springframework.context.ApplicationContext;
082
083import java.math.BigDecimal;
084import java.util.ArrayList;
085import java.util.Arrays;
086import java.util.Collection;
087import java.util.Collections;
088import java.util.Date;
089import java.util.HashSet;
090import java.util.List;
091import java.util.Objects;
092import java.util.Optional;
093import java.util.Set;
094import java.util.TreeSet;
095import java.util.stream.Collectors;
096import javax.measure.quantity.Quantity;
097import javax.measure.unit.NonSI;
098import javax.measure.unit.Unit;
099
100import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.DATE;
101import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.REFERENCE;
102import static org.apache.commons.lang3.StringUtils.isBlank;
103import static org.apache.commons.lang3.StringUtils.isNotBlank;
104import static org.apache.commons.lang3.StringUtils.trim;
105
106public abstract class BaseSearchParamExtractor implements ISearchParamExtractor {
107
108        public static final Set<String> COORDS_INDEX_PATHS;
109        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSearchParamExtractor.class);
110
111        static {
112                Set<String> coordsIndexPaths = Sets.newHashSet("Location.position");
113                COORDS_INDEX_PATHS = Collections.unmodifiableSet(coordsIndexPaths);
114        }
115
116        @Autowired
117        protected ApplicationContext myApplicationContext;
118
119        @Autowired
120        private FhirContext myContext;
121
122        @Autowired
123        private ISearchParamRegistry mySearchParamRegistry;
124
125        @Autowired
126        private StorageSettings myStorageSettings;
127
128        @Autowired
129        private PartitionSettings myPartitionSettings;
130
131        private Set<String> myIgnoredForSearchDatatypes;
132        private BaseRuntimeChildDefinition myQuantityValueValueChild;
133        private BaseRuntimeChildDefinition myQuantitySystemValueChild;
134        private BaseRuntimeChildDefinition myQuantityCodeValueChild;
135        private BaseRuntimeChildDefinition myMoneyValueChild;
136        private BaseRuntimeChildDefinition myMoneyCurrencyChild;
137        private BaseRuntimeElementCompositeDefinition<?> myLocationPositionDefinition;
138        private BaseRuntimeChildDefinition myCodeSystemUrlValueChild;
139        private BaseRuntimeChildDefinition myRangeLowValueChild;
140        private BaseRuntimeChildDefinition myRangeHighValueChild;
141        private BaseRuntimeChildDefinition myAddressLineValueChild;
142        private BaseRuntimeChildDefinition myAddressCityValueChild;
143        private BaseRuntimeChildDefinition myAddressDistrictValueChild;
144        private BaseRuntimeChildDefinition myAddressStateValueChild;
145        private BaseRuntimeChildDefinition myAddressCountryValueChild;
146        private BaseRuntimeChildDefinition myAddressPostalCodeValueChild;
147        private BaseRuntimeChildDefinition myCapabilityStatementRestSecurityServiceValueChild;
148        private BaseRuntimeChildDefinition myPeriodStartValueChild;
149        private BaseRuntimeChildDefinition myPeriodEndValueChild;
150        private BaseRuntimeChildDefinition myTimingEventValueChild;
151        private BaseRuntimeChildDefinition myTimingRepeatValueChild;
152        private BaseRuntimeChildDefinition myTimingRepeatBoundsValueChild;
153        private BaseRuntimeChildDefinition myDurationSystemValueChild;
154        private BaseRuntimeChildDefinition myDurationCodeValueChild;
155        private BaseRuntimeChildDefinition myDurationValueValueChild;
156        private BaseRuntimeChildDefinition myHumanNameFamilyValueChild;
157        private BaseRuntimeChildDefinition myHumanNameGivenValueChild;
158        private BaseRuntimeChildDefinition myHumanNameTextValueChild;
159        private BaseRuntimeChildDefinition myHumanNamePrefixValueChild;
160        private BaseRuntimeChildDefinition myHumanNameSuffixValueChild;
161        private BaseRuntimeChildDefinition myContactPointValueValueChild;
162        private BaseRuntimeChildDefinition myIdentifierSystemValueChild;
163        private BaseRuntimeChildDefinition myIdentifierValueValueChild;
164        private BaseRuntimeChildDefinition myIdentifierTypeValueChild;
165        private BaseRuntimeChildDefinition myIdentifierTypeTextValueChild;
166        private BaseRuntimeChildDefinition myCodeableConceptCodingValueChild;
167        private BaseRuntimeChildDefinition myCodeableConceptTextValueChild;
168        private BaseRuntimeChildDefinition myCodingSystemValueChild;
169        private BaseRuntimeChildDefinition myCodingCodeValueChild;
170        private BaseRuntimeChildDefinition myCodingDisplayValueChild;
171        private BaseRuntimeChildDefinition myContactPointSystemValueChild;
172        private BaseRuntimeChildDefinition myPatientCommunicationLanguageValueChild;
173        private BaseRuntimeChildDefinition myCodeableReferenceConcept;
174        private BaseRuntimeChildDefinition myCodeableReferenceReference;
175
176        // allow extraction of Resource-level search param values
177        private boolean myExtractResourceLevelParams = false;
178
179        /**
180         * Constructor
181         */
182        BaseSearchParamExtractor() {
183                super();
184        }
185
186        /**
187         * UNIT TEST constructor
188         */
189        BaseSearchParamExtractor(
190                        StorageSettings theStorageSettings,
191                        PartitionSettings thePartitionSettings,
192                        FhirContext theCtx,
193                        ISearchParamRegistry theSearchParamRegistry) {
194                Objects.requireNonNull(theStorageSettings);
195                Objects.requireNonNull(theCtx);
196                Objects.requireNonNull(theSearchParamRegistry);
197
198                myStorageSettings = theStorageSettings;
199                myContext = theCtx;
200                mySearchParamRegistry = theSearchParamRegistry;
201                myPartitionSettings = thePartitionSettings;
202        }
203
204        @Override
205        public SearchParamSet<PathAndRef> extractResourceLinks(IBaseResource theResource, boolean theWantLocalReferences) {
206                IExtractor<PathAndRef> extractor = createReferenceExtractor();
207                return extractSearchParams(
208                                theResource,
209                                extractor,
210                                RestSearchParameterTypeEnum.REFERENCE,
211                                theWantLocalReferences,
212                                ISearchParamExtractor.ALL_PARAMS);
213        }
214
215        private IExtractor<PathAndRef> createReferenceExtractor() {
216                return new ResourceLinkExtractor();
217        }
218
219        @Override
220        public PathAndRef extractReferenceLinkFromResource(IBase theValue, String thePath) {
221                ResourceLinkExtractor extractor = new ResourceLinkExtractor();
222                return extractor.get(theValue, thePath);
223        }
224
225        @Override
226        public List<String> extractParamValuesAsStrings(RuntimeSearchParam theSearchParam, IBaseResource theResource) {
227                IExtractor extractor = createExtractor(theSearchParam, theResource);
228
229                if (theSearchParam.getParamType().equals(REFERENCE)) {
230                        return extractReferenceParamsAsQueryTokens(theSearchParam, theResource, extractor);
231                } else {
232                        return extractParamsAsQueryTokens(theSearchParam, theResource, extractor);
233                }
234        }
235
236        @Nonnull
237        private IExtractor createExtractor(RuntimeSearchParam theSearchParam, IBaseResource theResource) {
238                IExtractor extractor;
239                switch (theSearchParam.getParamType()) {
240                        case DATE:
241                                extractor = createDateExtractor(theResource);
242                                break;
243                        case STRING:
244                                extractor = createStringExtractor(theResource);
245                                break;
246                        case TOKEN:
247                                extractor = createTokenExtractor(theResource);
248                                break;
249                        case NUMBER:
250                                extractor = createNumberExtractor(theResource);
251                                break;
252                        case REFERENCE:
253                                extractor = createReferenceExtractor();
254                                break;
255                        case QUANTITY:
256                                extractor = createQuantityExtractor(theResource);
257                                break;
258                        case URI:
259                                extractor = createUriExtractor(theResource);
260                                break;
261                        case SPECIAL:
262                                extractor = createSpecialExtractor(theResource.getIdElement().getResourceType());
263                                break;
264                        case COMPOSITE:
265                        case HAS:
266                        default:
267                                throw new UnsupportedOperationException(
268                                                Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction");
269                }
270                return extractor;
271        }
272
273        private List<String> extractReferenceParamsAsQueryTokens(
274                        RuntimeSearchParam theSearchParam, IBaseResource theResource, IExtractor<PathAndRef> theExtractor) {
275                SearchParamSet<PathAndRef> params = new SearchParamSet<>();
276                extractSearchParam(theSearchParam, theResource, theExtractor, params, false);
277                return refsToStringList(params);
278        }
279
280        private List<String> refsToStringList(SearchParamSet<PathAndRef> theParams) {
281                return theParams.stream()
282                                .map(PathAndRef::getRef)
283                                .map(ref -> ref.getReferenceElement().toUnqualifiedVersionless().getValue())
284                                .collect(Collectors.toList());
285        }
286
287        private <T extends BaseResourceIndexedSearchParam> List<String> extractParamsAsQueryTokens(
288                        RuntimeSearchParam theSearchParam, IBaseResource theResource, IExtractor<T> theExtractor) {
289                SearchParamSet<T> params = new SearchParamSet<>();
290                extractSearchParam(theSearchParam, theResource, theExtractor, params, false);
291                return toStringList(params);
292        }
293
294        private <T extends BaseResourceIndexedSearchParam> List<String> toStringList(SearchParamSet<T> theParams) {
295                return theParams.stream()
296                                .map(param -> param.toQueryParameterType().getValueAsQueryToken(myContext))
297                                .collect(Collectors.toList());
298        }
299
300        @Override
301        public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(
302                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
303                IExtractor<ResourceIndexedSearchParamComposite> extractor = createCompositeExtractor(theResource);
304                return extractSearchParams(
305                                theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false, theSearchParamFilter);
306        }
307
308        private IExtractor<ResourceIndexedSearchParamComposite> createCompositeExtractor(IBaseResource theResource) {
309                return new CompositeExtractor(theResource);
310        }
311
312        /**
313         * Extractor for composite SPs.
314         * Extracts elements, and then recurses and applies extractors for each component SP using the element as the new root.
315         */
316        public class CompositeExtractor implements IExtractor<ResourceIndexedSearchParamComposite> {
317                final IBaseResource myResource;
318                final String myResourceType;
319
320                public CompositeExtractor(IBaseResource theResource) {
321                        myResource = theResource;
322                        myResourceType = toRootTypeName(theResource);
323                }
324
325                /**
326                 * Extract the subcomponent index data for each component of a composite SP from an IBase element.
327                 *
328                 * @param theParams               will add 1 or 0 ResourceIndexedSearchParamComposite instances for theValue
329                 * @param theCompositeSearchParam the composite SP
330                 * @param theValue                the focus element for the subcomponent extraction
331                 * @param thePath                 unused api param
332                 * @param theWantLocalReferences  passed down to reference extraction
333                 */
334                @Override
335                public void extract(
336                                SearchParamSet<ResourceIndexedSearchParamComposite> theParams,
337                                RuntimeSearchParam theCompositeSearchParam,
338                                IBase theValue,
339                                String thePath,
340                                boolean theWantLocalReferences) {
341
342                        // skip broken SPs
343                        if (!isExtractableComposite(theCompositeSearchParam)) {
344                                ourLog.info(
345                                                "CompositeExtractor - skipping unsupported search parameter {}",
346                                                theCompositeSearchParam.getName());
347                                return;
348                        }
349
350                        String compositeSpName = theCompositeSearchParam.getName();
351                        ourLog.trace("CompositeExtractor - extracting {} {}", compositeSpName, theValue);
352                        ResourceIndexedSearchParamComposite e = new ResourceIndexedSearchParamComposite(compositeSpName, thePath);
353
354                        // extract the index data for each component.
355                        for (RuntimeSearchParam.Component component : theCompositeSearchParam.getComponents()) {
356                                String componentSpRef = component.getReference();
357                                String expression = component.getExpression();
358
359                                RuntimeSearchParam componentSp = mySearchParamRegistry.getActiveSearchParamByUrl(
360                                                componentSpRef, ISearchParamRegistry.SearchParamLookupContextEnum.INDEX);
361                                Validate.notNull(
362                                                componentSp,
363                                                "Misconfigured SP %s - failed to load component %s",
364                                                compositeSpName,
365                                                componentSpRef);
366
367                                SearchParamSet<BaseResourceIndexedSearchParam> componentIndexedSearchParams =
368                                                extractCompositeComponentIndexData(
369                                                                theValue, componentSp, expression, theWantLocalReferences, theCompositeSearchParam);
370                                if (componentIndexedSearchParams.isEmpty()) {
371                                        // If any of the components are empty, no search can ever match.  Short circuit, and bail out.
372                                        return;
373                                } else {
374                                        e.addComponentIndexedSearchParams(componentSp, componentIndexedSearchParams);
375                                }
376                        }
377
378                        // every component has data.  Add it for indexing.
379                        theParams.add(e);
380                }
381
382                /**
383                 * Extract the subcomponent index data for a single component of a composite SP.
384                 *
385                 * @param theFocusElement         the element to use as the root for sub-extraction
386                 * @param theComponentSearchParam the active subcomponent SP for extraction
387                 * @param theSubPathExpression    the sub-expression to extract values from theFocusElement
388                 * @param theWantLocalReferences  flag for URI processing
389                 * @param theCompositeSearchParam the parent composite SP
390                 * @return the extracted index beans for theFocusElement
391                 */
392                @Nonnull
393                private SearchParamSet<BaseResourceIndexedSearchParam> extractCompositeComponentIndexData(
394                                IBase theFocusElement,
395                                RuntimeSearchParam theComponentSearchParam,
396                                String theSubPathExpression,
397                                boolean theWantLocalReferences,
398                                RuntimeSearchParam theCompositeSearchParam) {
399                        IExtractor componentExtractor = createExtractor(theComponentSearchParam, myResource);
400                        SearchParamSet<BaseResourceIndexedSearchParam> componentIndexData = new SearchParamSet<>();
401
402                        extractSearchParam(
403                                        theComponentSearchParam,
404                                        theSubPathExpression,
405                                        theFocusElement,
406                                        componentExtractor,
407                                        componentIndexData,
408                                        theWantLocalReferences);
409                        ourLog.trace(
410                                        "CompositeExtractor - extracted {} index values for {}",
411                                        componentIndexData.size(),
412                                        theComponentSearchParam.getName());
413
414                        return componentIndexData;
415                }
416
417                /**
418                 * Is this an extractable composite SP?
419                 *
420                 * @param theSearchParam of type composite
421                 * @return can we extract useful index data from this?
422                 */
423                private boolean isExtractableComposite(RuntimeSearchParam theSearchParam) {
424                        // this is a composite SP
425                        return RestSearchParameterTypeEnum.COMPOSITE.equals(theSearchParam.getParamType())
426                                        && theSearchParam.getComponents().stream().noneMatch(this::isNotExtractableCompositeComponent);
427                }
428
429                private boolean isNotExtractableCompositeComponent(RuntimeSearchParam.Component c) {
430                        RuntimeSearchParam componentSearchParam = mySearchParamRegistry.getActiveSearchParamByUrl(
431                                        c.getReference(), ISearchParamRegistry.SearchParamLookupContextEnum.INDEX);
432                        return // Does the sub-param link work?
433                        componentSearchParam == null
434                                        ||
435                                        // Is this the right type?
436                                        RestSearchParameterTypeEnum.COMPOSITE.equals(componentSearchParam.getParamType())
437                                        ||
438
439                                        // Bug workaround: the component expressions are null in the FhirContextSearchParamRegistry. We
440                                        // can't do anything with them.
441                                        c.getExpression() == null
442                                        ||
443
444                                        // TODO mb Bug workaround: we don't support the %resource variable, but standard SPs on
445                                        // MolecularSequence use it.
446                                        // Skip them for now.
447                                        c.getExpression().contains("%resource");
448                }
449        }
450
451        @Override
452        public SearchParamSet<ResourceIndexedComboStringUnique> extractSearchParamComboUnique(
453                        String theResourceType, ResourceIndexedSearchParams theParams) {
454                SearchParamSet<ResourceIndexedComboStringUnique> retVal = new SearchParamSet<>();
455                List<RuntimeSearchParam> runtimeComboUniqueParams = mySearchParamRegistry.getActiveComboSearchParams(
456                                theResourceType, ComboSearchParamType.UNIQUE, ISearchParamRegistry.SearchParamLookupContextEnum.INDEX);
457
458                for (RuntimeSearchParam runtimeParam : runtimeComboUniqueParams) {
459                        Set<ResourceIndexedComboStringUnique> comboUniqueParams =
460                                        createComboUniqueParam(theResourceType, theParams, runtimeParam);
461                        retVal.addAll(comboUniqueParams);
462                }
463                return retVal;
464        }
465
466        private SearchParamSet<ResourceIndexedComboStringUnique> createComboUniqueParam(
467                        String theResourceType, ResourceIndexedSearchParams theParams, RuntimeSearchParam theRuntimeParam) {
468                SearchParamSet<ResourceIndexedComboStringUnique> retVal = new SearchParamSet<>();
469                Set<String> queryStringsToPopulate =
470                                extractParameterCombinationsForComboParam(theParams, theResourceType, theRuntimeParam);
471
472                for (String nextQueryString : queryStringsToPopulate) {
473                        ourLog.trace(
474                                        "Adding composite unique SP: {} on {} for {}",
475                                        nextQueryString,
476                                        theResourceType,
477                                        theRuntimeParam.getId());
478                        ResourceIndexedComboStringUnique uniqueParam = new ResourceIndexedComboStringUnique();
479                        uniqueParam.setIndexString(nextQueryString);
480                        uniqueParam.setSearchParameterId(theRuntimeParam.getId());
481                        retVal.add(uniqueParam);
482                }
483                return retVal;
484        }
485
486        @Override
487        public SearchParamSet<ResourceIndexedComboTokenNonUnique> extractSearchParamComboNonUnique(
488                        String theResourceType, ResourceIndexedSearchParams theParams) {
489                SearchParamSet<ResourceIndexedComboTokenNonUnique> retVal = new SearchParamSet<>();
490                List<RuntimeSearchParam> runtimeComboNonUniqueParams = mySearchParamRegistry.getActiveComboSearchParams(
491                                theResourceType,
492                                ComboSearchParamType.NON_UNIQUE,
493                                ISearchParamRegistry.SearchParamLookupContextEnum.INDEX);
494
495                for (RuntimeSearchParam runtimeParam : runtimeComboNonUniqueParams) {
496                        Set<ResourceIndexedComboTokenNonUnique> comboNonUniqueParams =
497                                        createComboNonUniqueParam(theResourceType, theParams, runtimeParam);
498                        retVal.addAll(comboNonUniqueParams);
499                }
500                return retVal;
501        }
502
503        private SearchParamSet<ResourceIndexedComboTokenNonUnique> createComboNonUniqueParam(
504                        String theResourceType, ResourceIndexedSearchParams theParams, RuntimeSearchParam theRuntimeParam) {
505                SearchParamSet<ResourceIndexedComboTokenNonUnique> retVal = new SearchParamSet<>();
506                Set<String> queryStringsToPopulate =
507                                extractParameterCombinationsForComboParam(theParams, theResourceType, theRuntimeParam);
508
509                for (String nextQueryString : queryStringsToPopulate) {
510                        ourLog.trace("Adding composite unique SP: {}", nextQueryString);
511                        ResourceIndexedComboTokenNonUnique nonUniqueParam = new ResourceIndexedComboTokenNonUnique();
512                        nonUniqueParam.setPartitionSettings(myPartitionSettings);
513                        nonUniqueParam.setIndexString(nextQueryString);
514                        nonUniqueParam.setSearchParameterId(theRuntimeParam.getId());
515                        retVal.add(nonUniqueParam);
516                }
517                return retVal;
518        }
519
520        @Nonnull
521        private Set<String> extractParameterCombinationsForComboParam(
522                        ResourceIndexedSearchParams theParams, String theResourceType, RuntimeSearchParam theParam) {
523                List<List<String>> partsChoices = new ArrayList<>();
524
525                List<RuntimeSearchParam> compositeComponents =
526                                JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam);
527                for (RuntimeSearchParam nextCompositeOf : compositeComponents) {
528                        Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart =
529                                        findParameterIndexes(theParams, nextCompositeOf);
530
531                        Collection<ResourceLink> linksForCompositePart = null;
532                        switch (nextCompositeOf.getParamType()) {
533                                case REFERENCE:
534                                        linksForCompositePart = theParams.myLinks;
535                                        break;
536                                case NUMBER:
537                                case DATE:
538                                case STRING:
539                                case TOKEN:
540                                case QUANTITY:
541                                case URI:
542                                case SPECIAL:
543                                case COMPOSITE:
544                                case HAS:
545                                        break;
546                        }
547
548                        Collection<String> linksForCompositePartWantPaths = null;
549                        switch (nextCompositeOf.getParamType()) {
550                                case REFERENCE:
551                                        linksForCompositePartWantPaths = new HashSet<>(nextCompositeOf.getPathsSplit());
552                                        break;
553                                case NUMBER:
554                                case DATE:
555                                case STRING:
556                                case TOKEN:
557                                case QUANTITY:
558                                case URI:
559                                case SPECIAL:
560                                case COMPOSITE:
561                                case HAS:
562                                        break;
563                        }
564
565                        ArrayList<String> nextChoicesList = new ArrayList<>();
566                        partsChoices.add(nextChoicesList);
567
568                        String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
569                        if (paramsListForCompositePart != null) {
570                                for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
571                                        IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
572
573                                        if (theParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE
574                                                        && nextParamAsClientParam instanceof DateParam) {
575                                                DateParam date = (DateParam) nextParamAsClientParam;
576                                                if (date.getPrecision() != TemporalPrecisionEnum.DAY) {
577                                                        continue;
578                                                }
579                                        }
580
581                                        String value = nextParamAsClientParam.getValueAsQueryToken(myContext);
582
583                                        RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
584                                                        theResourceType, key, ISearchParamRegistry.SearchParamLookupContextEnum.INDEX);
585                                        if (theParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE
586                                                        && param != null
587                                                        && param.getParamType() == RestSearchParameterTypeEnum.STRING) {
588                                                value = StringUtil.normalizeStringForSearchIndexing(value);
589                                        }
590
591                                        if (isNotBlank(value)) {
592                                                value = UrlUtil.escapeUrlParam(value);
593                                                nextChoicesList.add(key + "=" + value);
594                                        }
595                                }
596                        }
597
598                        if (linksForCompositePart != null) {
599                                for (ResourceLink nextLink : linksForCompositePart) {
600                                        if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
601                                                assert isNotBlank(nextLink.getTargetResourceType());
602                                                assert isNotBlank(nextLink.getTargetResourceId());
603                                                String value = nextLink.getTargetResourceType() + "/" + nextLink.getTargetResourceId();
604                                                if (isNotBlank(value)) {
605                                                        value = UrlUtil.escapeUrlParam(value);
606                                                        nextChoicesList.add(key + "=" + value);
607                                                }
608                                        }
609                                }
610                        }
611                }
612
613                return ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains(theResourceType, partsChoices);
614        }
615
616        @Nullable
617        private Collection<? extends BaseResourceIndexedSearchParam> findParameterIndexes(
618                        ResourceIndexedSearchParams theParams, RuntimeSearchParam nextCompositeOf) {
619                Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
620                switch (nextCompositeOf.getParamType()) {
621                        case NUMBER:
622                                paramsListForCompositePart = theParams.myNumberParams;
623                                break;
624                        case DATE:
625                                paramsListForCompositePart = theParams.myDateParams;
626                                break;
627                        case STRING:
628                                paramsListForCompositePart = theParams.myStringParams;
629                                break;
630                        case TOKEN:
631                                paramsListForCompositePart = theParams.myTokenParams;
632                                break;
633                        case QUANTITY:
634                                paramsListForCompositePart = theParams.myQuantityParams;
635                                break;
636                        case URI:
637                                paramsListForCompositePart = theParams.myUriParams;
638                                break;
639                        case REFERENCE:
640                        case SPECIAL:
641                        case COMPOSITE:
642                        case HAS:
643                                break;
644                }
645                if (paramsListForCompositePart != null) {
646                        paramsListForCompositePart = paramsListForCompositePart.stream()
647                                        .filter(t -> t.getParamName().equals(nextCompositeOf.getName()))
648                                        .collect(Collectors.toList());
649                }
650                return paramsListForCompositePart;
651        }
652
653        @Override
654        public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(
655                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
656                IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource);
657                return extractSearchParams(
658                                theResource, extractor, RestSearchParameterTypeEnum.TOKEN, false, theSearchParamFilter);
659        }
660
661        @Override
662        public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(
663                        IBaseResource theResource, RuntimeSearchParam theSearchParam) {
664                IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource);
665                SearchParamSet<BaseResourceIndexedSearchParam> setToPopulate = new SearchParamSet<>();
666                extractSearchParam(theSearchParam, theResource, extractor, setToPopulate, false);
667                return setToPopulate;
668        }
669
670        private IExtractor<BaseResourceIndexedSearchParam> createTokenExtractor(IBaseResource theResource) {
671                String resourceTypeName = toRootTypeName(theResource);
672                String useSystem;
673                if (getContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU2)) {
674                        if (resourceTypeName.equals("ValueSet")) {
675                                ca.uhn.fhir.model.dstu2.resource.ValueSet dstu2ValueSet =
676                                                (ca.uhn.fhir.model.dstu2.resource.ValueSet) theResource;
677                                useSystem = dstu2ValueSet.getCodeSystem().getSystem();
678                        } else {
679                                useSystem = null;
680                        }
681                } else {
682                        if (resourceTypeName.equals("CodeSystem")) {
683                                useSystem = extractValueAsString(myCodeSystemUrlValueChild, theResource);
684                        } else {
685                                useSystem = null;
686                        }
687                }
688
689                return new TokenExtractor(resourceTypeName, useSystem);
690        }
691
692        @Override
693        public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(
694                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
695                String resourceTypeName = toRootTypeName(theResource);
696                IExtractor<BaseResourceIndexedSearchParam> extractor = createSpecialExtractor(resourceTypeName);
697                return extractSearchParams(
698                                theResource, extractor, RestSearchParameterTypeEnum.SPECIAL, false, theSearchParamFilter);
699        }
700
701        private IExtractor<BaseResourceIndexedSearchParam> createSpecialExtractor(String theResourceTypeName) {
702                return (params, searchParam, value, path, theWantLocalReferences) -> {
703                        if (COORDS_INDEX_PATHS.contains(path)) {
704                                addCoords_Position(theResourceTypeName, params, searchParam, value);
705                        }
706                };
707        }
708
709        private void addUnexpectedDatatypeWarning(
710                        SearchParamSet<?> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath) {
711                String typeDesc = myContext.getElementDefinition(theValue.getClass()).getName();
712                theParams.addWarning("Search param " + theSearchParam.getBase() + "#" + theSearchParam.getName()
713                                + " is unable to index value of type " + typeDesc + " as a "
714                                + theSearchParam.getParamType().name() + " at path: " + thePath);
715        }
716
717        @Override
718        public SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(
719                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
720                IExtractor<ResourceIndexedSearchParamUri> extractor = createUriExtractor(theResource);
721                return extractSearchParams(
722                                theResource, extractor, RestSearchParameterTypeEnum.URI, false, theSearchParamFilter);
723        }
724
725        private IExtractor<ResourceIndexedSearchParamUri> createUriExtractor(IBaseResource theResource) {
726                return (params, searchParam, value, path, theWantLocalReferences) -> {
727                        String nextType = toRootTypeName(value);
728                        String resourceType = toRootTypeName(theResource);
729                        switch (nextType) {
730                                case "uri":
731                                case "url":
732                                case "oid":
733                                case "sid":
734                                case "uuid":
735                                        addUri_Uri(resourceType, params, searchParam, value);
736                                        break;
737                                default:
738                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
739                                        break;
740                        }
741                };
742        }
743
744        @Override
745        public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(
746                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
747                IExtractor<ResourceIndexedSearchParamDate> extractor = createDateExtractor(theResource);
748                return extractSearchParams(theResource, extractor, DATE, false, theSearchParamFilter);
749        }
750
751        private IExtractor<ResourceIndexedSearchParamDate> createDateExtractor(IBaseResource theResource) {
752                return new DateExtractor(theResource);
753        }
754
755        @Override
756        public Date extractDateFromResource(IBase theValue, String thePath) {
757                DateExtractor extractor = new DateExtractor("DateType");
758                return extractor.get(theValue, thePath, false).getValueHigh();
759        }
760
761        @Override
762        public SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(
763                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
764                IExtractor<ResourceIndexedSearchParamNumber> extractor = createNumberExtractor(theResource);
765                return extractSearchParams(
766                                theResource, extractor, RestSearchParameterTypeEnum.NUMBER, false, theSearchParamFilter);
767        }
768
769        private IExtractor<ResourceIndexedSearchParamNumber> createNumberExtractor(IBaseResource theResource) {
770                return (params, searchParam, value, path, theWantLocalReferences) -> {
771                        String nextType = toRootTypeName(value);
772                        String resourceType = toRootTypeName(theResource);
773                        switch (nextType) {
774                                case "Duration":
775                                        addNumber_Duration(resourceType, params, searchParam, value);
776                                        break;
777                                case "Quantity":
778                                        addNumber_Quantity(resourceType, params, searchParam, value);
779                                        break;
780                                case "integer":
781                                case "positiveInt":
782                                case "unsignedInt":
783                                        addNumber_Integer(resourceType, params, searchParam, value);
784                                        break;
785                                case "decimal":
786                                        addNumber_Decimal(resourceType, params, searchParam, value);
787                                        break;
788                                case "Range":
789                                        addNumber_Range(resourceType, params, searchParam, value);
790                                        break;
791                                default:
792                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
793                                        break;
794                        }
795                };
796        }
797
798        @Override
799        public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(
800                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
801                IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityUnnormalizedExtractor(theResource);
802                return extractSearchParams(
803                                theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theSearchParamFilter);
804        }
805
806        @Override
807        public SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> extractSearchParamQuantityNormalized(
808                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
809                IExtractor<ResourceIndexedSearchParamQuantityNormalized> extractor =
810                                createQuantityNormalizedExtractor(theResource);
811                return extractSearchParams(
812                                theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false, theSearchParamFilter);
813        }
814
815        @Nonnull
816        private IExtractor<? extends BaseResourceIndexedSearchParamQuantity> createQuantityExtractor(
817                        IBaseResource theResource) {
818                IExtractor<? extends BaseResourceIndexedSearchParamQuantity> result;
819                if (myStorageSettings.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) {
820                        result = new MultiplexExtractor(
821                                        createQuantityUnnormalizedExtractor(theResource), createQuantityNormalizedExtractor(theResource));
822                } else {
823                        result = createQuantityUnnormalizedExtractor(theResource);
824                }
825                return result;
826        }
827
828        @Nonnull
829        private IExtractor<ResourceIndexedSearchParamQuantity> createQuantityUnnormalizedExtractor(
830                        IBaseResource theResource) {
831                String resourceType = toRootTypeName(theResource);
832                return (params, searchParam, value, path, theWantLocalReferences) -> {
833                        if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) {
834                                return;
835                        }
836
837                        String nextType = toRootTypeName(value);
838                        switch (nextType) {
839                                case "Quantity":
840                                        addQuantity_Quantity(resourceType, params, searchParam, value);
841                                        break;
842                                case "Money":
843                                        addQuantity_Money(resourceType, params, searchParam, value);
844                                        break;
845                                case "Range":
846                                        addQuantity_Range(resourceType, params, searchParam, value);
847                                        break;
848                                default:
849                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
850                                        break;
851                        }
852                };
853        }
854
855        private IExtractor<ResourceIndexedSearchParamQuantityNormalized> createQuantityNormalizedExtractor(
856                        IBaseResource theResource) {
857
858                return (params, searchParam, value, path, theWantLocalReferences) -> {
859                        if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) {
860                                return;
861                        }
862
863                        String nextType = toRootTypeName(value);
864                        String resourceType = toRootTypeName(theResource);
865                        switch (nextType) {
866                                case "Quantity":
867                                        addQuantity_QuantityNormalized(resourceType, params, searchParam, value);
868                                        break;
869                                case "Money":
870                                        addQuantity_MoneyNormalized(resourceType, params, searchParam, value);
871                                        break;
872                                case "Range":
873                                        addQuantity_RangeNormalized(resourceType, params, searchParam, value);
874                                        break;
875                                default:
876                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
877                                        break;
878                        }
879                };
880        }
881
882        @Override
883        public SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(
884                        IBaseResource theResource, ISearchParamFilter theSearchParamFilter) {
885                IExtractor<ResourceIndexedSearchParamString> extractor = createStringExtractor(theResource);
886
887                return extractSearchParams(
888                                theResource, extractor, RestSearchParameterTypeEnum.STRING, false, theSearchParamFilter);
889        }
890
891        private IExtractor<ResourceIndexedSearchParamString> createStringExtractor(IBaseResource theResource) {
892                String resourceType = toRootTypeName(theResource);
893                return (params, searchParam, value, path, theWantLocalReferences) -> {
894                        if (value instanceof IPrimitiveType) {
895                                IPrimitiveType<?> nextValue = (IPrimitiveType<?>) value;
896                                String valueAsString = nextValue.getValueAsString();
897                                createStringIndexIfNotBlank(resourceType, params, searchParam, valueAsString);
898                                return;
899                        }
900
901                        String nextType = toRootTypeName(value);
902                        switch (nextType) {
903                                case "HumanName":
904                                        addString_HumanName(resourceType, params, searchParam, value);
905                                        break;
906                                case "Address":
907                                        addString_Address(resourceType, params, searchParam, value);
908                                        break;
909                                case "ContactPoint":
910                                        addString_ContactPoint(resourceType, params, searchParam, value);
911                                        break;
912                                case "Quantity":
913                                        addString_Quantity(resourceType, params, searchParam, value);
914                                        break;
915                                case "Range":
916                                        addString_Range(resourceType, params, searchParam, value);
917                                        break;
918                                case "Period":
919                                        // Condition.onset[x] can have a Period - Ignored for now
920                                        break;
921                                default:
922                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
923                                        break;
924                        }
925                };
926        }
927
928        /**
929         * Override parent because we're using FHIRPath here
930         */
931        @Override
932        public List<IBase> extractValues(String thePaths, IBase theResource) {
933                List<IBase> values = new ArrayList<>();
934                if (isNotBlank(thePaths)) {
935                        String[] nextPathsSplit = split(thePaths);
936                        for (String nextPath : nextPathsSplit) {
937                                List<? extends IBase> allValues;
938
939                                // This path is hard to parse and isn't likely to produce anything useful anyway
940                                if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU2)
941                                                && nextPath.equals("Bundle.entry.resource(0)")) {
942                                        continue;
943                                }
944
945                                nextPath = trim(nextPath);
946                                IValueExtractor allValuesFunc = getPathValueExtractor(theResource, nextPath);
947                                try {
948                                        allValues = allValuesFunc.get();
949                                } catch (Exception e) {
950                                        String msg = getContext()
951                                                        .getLocalizer()
952                                                        .getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
953                                        throw new InternalErrorException(Msg.code(504) + msg, e);
954                                }
955
956                                values.addAll(allValues);
957                        }
958
959                        for (int i = 0; i < values.size(); i++) {
960                                IBase nextObject = values.get(i);
961                                if (nextObject instanceof IBaseExtension) {
962                                        IBaseExtension<?, ?> nextExtension = (IBaseExtension<?, ?>) nextObject;
963                                        nextObject = nextExtension.getValue();
964                                        values.set(i, nextObject);
965                                }
966                        }
967                }
968
969                return values;
970        }
971
972        protected FhirContext getContext() {
973                return myContext;
974        }
975
976        @VisibleForTesting
977        public void setContext(FhirContext theContext) {
978                myContext = theContext;
979        }
980
981        protected StorageSettings getStorageSettings() {
982                return myStorageSettings;
983        }
984
985        @VisibleForTesting
986        public void setStorageSettings(StorageSettings theStorageSettings) {
987                myStorageSettings = theStorageSettings;
988        }
989
990        @VisibleForTesting
991        public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
992                mySearchParamRegistry = theSearchParamRegistry;
993        }
994
995        @VisibleForTesting
996        Collection<RuntimeSearchParam> getSearchParams(IBaseResource theResource) {
997                RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource);
998                Collection<RuntimeSearchParam> retVal = mySearchParamRegistry
999                                .getActiveSearchParams(def.getName(), ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)
1000                                .values();
1001                List<RuntimeSearchParam> defaultList = Collections.emptyList();
1002                retVal = ObjectUtils.defaultIfNull(retVal, defaultList);
1003                return retVal;
1004        }
1005
1006        private void addQuantity_Quantity(
1007                        String theResourceType,
1008                        Set<ResourceIndexedSearchParamQuantity> theParams,
1009                        RuntimeSearchParam theSearchParam,
1010                        IBase theValue) {
1011                Optional<IPrimitiveType<BigDecimal>> valueField =
1012                                myQuantityValueValueChild.getAccessor().getFirstValueOrNull(theValue);
1013                if (valueField.isPresent() && valueField.get().getValue() != null) {
1014                        BigDecimal nextValueValue = valueField.get().getValue();
1015                        String system = extractValueAsString(myQuantitySystemValueChild, theValue);
1016                        String code = extractValueAsString(myQuantityCodeValueChild, theValue);
1017
1018                        ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(
1019                                        myPartitionSettings, theResourceType, theSearchParam.getName(), nextValueValue, system, code);
1020
1021                        theParams.add(nextEntity);
1022                }
1023        }
1024
1025        private void addQuantity_QuantityNormalized(
1026                        String theResourceType,
1027                        Set<ResourceIndexedSearchParamQuantityNormalized> theParams,
1028                        RuntimeSearchParam theSearchParam,
1029                        IBase theValue) {
1030                Optional<IPrimitiveType<BigDecimal>> valueField =
1031                                myQuantityValueValueChild.getAccessor().getFirstValueOrNull(theValue);
1032                if (valueField.isPresent() && valueField.get().getValue() != null) {
1033                        BigDecimal nextValueValue = valueField.get().getValue();
1034                        String system = extractValueAsString(myQuantitySystemValueChild, theValue);
1035                        String code = extractValueAsString(myQuantityCodeValueChild, theValue);
1036
1037                        // -- convert the value/unit to the canonical form if any
1038                        Pair canonicalForm = UcumServiceUtil.getCanonicalForm(system, nextValueValue, code);
1039                        if (canonicalForm != null) {
1040                                double canonicalValue =
1041                                                Double.parseDouble(canonicalForm.getValue().asDecimal());
1042                                String canonicalUnits = canonicalForm.getCode();
1043                                ResourceIndexedSearchParamQuantityNormalized nextEntity =
1044                                                new ResourceIndexedSearchParamQuantityNormalized(
1045                                                                myPartitionSettings,
1046                                                                theResourceType,
1047                                                                theSearchParam.getName(),
1048                                                                canonicalValue,
1049                                                                system,
1050                                                                canonicalUnits);
1051                                theParams.add(nextEntity);
1052                        }
1053                }
1054        }
1055
1056        private void addQuantity_Money(
1057                        String theResourceType,
1058                        Set<ResourceIndexedSearchParamQuantity> theParams,
1059                        RuntimeSearchParam theSearchParam,
1060                        IBase theValue) {
1061                Optional<IPrimitiveType<BigDecimal>> valueField =
1062                                myMoneyValueChild.getAccessor().getFirstValueOrNull(theValue);
1063                if (valueField.isPresent() && valueField.get().getValue() != null) {
1064                        BigDecimal nextValueValue = valueField.get().getValue();
1065
1066                        String nextValueString = "urn:iso:std:iso:4217";
1067                        String nextValueCode = extractValueAsString(myMoneyCurrencyChild, theValue);
1068                        String searchParamName = theSearchParam.getName();
1069
1070                        ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(
1071                                        myPartitionSettings,
1072                                        theResourceType,
1073                                        searchParamName,
1074                                        nextValueValue,
1075                                        nextValueString,
1076                                        nextValueCode);
1077                        theParams.add(nextEntity);
1078                }
1079        }
1080
1081        private void addQuantity_MoneyNormalized(
1082                        String theResourceType,
1083                        Set<ResourceIndexedSearchParamQuantityNormalized> theParams,
1084                        RuntimeSearchParam theSearchParam,
1085                        IBase theValue) {
1086                Optional<IPrimitiveType<BigDecimal>> valueField =
1087                                myMoneyValueChild.getAccessor().getFirstValueOrNull(theValue);
1088                if (valueField.isPresent() && valueField.get().getValue() != null) {
1089                        BigDecimal nextValueValue = valueField.get().getValue();
1090
1091                        String nextValueString = "urn:iso:std:iso:4217";
1092                        String nextValueCode = extractValueAsString(myMoneyCurrencyChild, theValue);
1093                        String searchParamName = theSearchParam.getName();
1094
1095                        ResourceIndexedSearchParamQuantityNormalized nextEntityNormalized =
1096                                        new ResourceIndexedSearchParamQuantityNormalized(
1097                                                        myPartitionSettings,
1098                                                        theResourceType,
1099                                                        searchParamName,
1100                                                        nextValueValue.doubleValue(),
1101                                                        nextValueString,
1102                                                        nextValueCode);
1103                        theParams.add(nextEntityNormalized);
1104                }
1105        }
1106
1107        private void addQuantity_Range(
1108                        String theResourceType,
1109                        Set<ResourceIndexedSearchParamQuantity> theParams,
1110                        RuntimeSearchParam theSearchParam,
1111                        IBase theValue) {
1112                Optional<IBase> low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
1113                low.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase));
1114
1115                Optional<IBase> high = myRangeHighValueChild.getAccessor().getFirstValueOrNull(theValue);
1116                high.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase));
1117        }
1118
1119        private void addQuantity_RangeNormalized(
1120                        String theResourceType,
1121                        Set<ResourceIndexedSearchParamQuantityNormalized> theParams,
1122                        RuntimeSearchParam theSearchParam,
1123                        IBase theValue) {
1124                Optional<IBase> low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
1125                low.ifPresent(theIBase -> addQuantity_QuantityNormalized(theResourceType, theParams, theSearchParam, theIBase));
1126
1127                Optional<IBase> high = myRangeHighValueChild.getAccessor().getFirstValueOrNull(theValue);
1128                high.ifPresent(
1129                                theIBase -> addQuantity_QuantityNormalized(theResourceType, theParams, theSearchParam, theIBase));
1130        }
1131
1132        @SuppressWarnings("unchecked")
1133        private void addToken_Identifier(
1134                        String theResourceType,
1135                        Set<BaseResourceIndexedSearchParam> theParams,
1136                        RuntimeSearchParam theSearchParam,
1137                        IBase theValue) {
1138                String system = extractValueAsString(myIdentifierSystemValueChild, theValue);
1139                String value = extractValueAsString(myIdentifierValueValueChild, theValue);
1140                if (isNotBlank(value)) {
1141                        createTokenIndexIfNotBlankAndAdd(theResourceType, theParams, theSearchParam, system, value);
1142
1143                        boolean indexIdentifierType = myStorageSettings.isIndexIdentifierOfType();
1144                        if (indexIdentifierType) {
1145                                Optional<IBase> type = myIdentifierTypeValueChild.getAccessor().getFirstValueOrNull(theValue);
1146                                if (type.isPresent()) {
1147                                        List<IBase> codings =
1148                                                        myCodeableConceptCodingValueChild.getAccessor().getValues(type.get());
1149                                        for (IBase nextCoding : codings) {
1150
1151                                                String typeSystem = myCodingSystemValueChild
1152                                                                .getAccessor()
1153                                                                .getFirstValueOrNull(nextCoding)
1154                                                                .map(t -> ((IPrimitiveType<String>) t).getValue())
1155                                                                .orElse(null);
1156                                                String typeValue = myCodingCodeValueChild
1157                                                                .getAccessor()
1158                                                                .getFirstValueOrNull(nextCoding)
1159                                                                .map(t -> ((IPrimitiveType<String>) t).getValue())
1160                                                                .orElse(null);
1161                                                if (isNotBlank(typeSystem) && isNotBlank(typeValue)) {
1162                                                        String paramName = theSearchParam.getName() + Constants.PARAMQUALIFIER_TOKEN_OF_TYPE;
1163                                                        ResourceIndexedSearchParamToken token = createTokenIndexIfNotBlank(
1164                                                                        theResourceType, typeSystem, typeValue + "|" + value, paramName);
1165                                                        if (token != null) {
1166                                                                theParams.add(token);
1167                                                        }
1168                                                }
1169                                        }
1170                                }
1171                        }
1172                }
1173        }
1174
1175        protected boolean shouldIndexTextComponentOfToken(RuntimeSearchParam theSearchParam) {
1176                return tokenTextIndexingEnabledForSearchParam(myStorageSettings, theSearchParam);
1177        }
1178
1179        private void addToken_CodeableConcept(
1180                        String theResourceType,
1181                        Set<BaseResourceIndexedSearchParam> theParams,
1182                        RuntimeSearchParam theSearchParam,
1183                        IBase theValue) {
1184                List<IBase> codings = getCodingsFromCodeableConcept(theValue);
1185                for (IBase nextCoding : codings) {
1186                        addToken_Coding(theResourceType, theParams, theSearchParam, nextCoding);
1187                }
1188
1189                if (shouldIndexTextComponentOfToken(theSearchParam)) {
1190                        String text = getDisplayTextFromCodeableConcept(theValue);
1191                        if (isNotBlank(text)) {
1192                                createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text);
1193                        }
1194                }
1195        }
1196
1197        @Override
1198        public List<IBase> getCodingsFromCodeableConcept(IBase theValue) {
1199                String nextType = BaseSearchParamExtractor.this.toRootTypeName(theValue);
1200                if ("CodeableConcept".equals(nextType)) {
1201                        return myCodeableConceptCodingValueChild.getAccessor().getValues(theValue);
1202                } else {
1203                        return null;
1204                }
1205        }
1206
1207        @Override
1208        public String getDisplayTextFromCodeableConcept(IBase theValue) {
1209                String nextType = BaseSearchParamExtractor.this.toRootTypeName(theValue);
1210                if ("CodeableConcept".equals(nextType)) {
1211                        return extractValueAsString(myCodeableConceptTextValueChild, theValue);
1212                } else {
1213                        return null;
1214                }
1215        }
1216
1217        private void addToken_Coding(
1218                        String theResourceType,
1219                        Set<BaseResourceIndexedSearchParam> theParams,
1220                        RuntimeSearchParam theSearchParam,
1221                        IBase theValue) {
1222                ResourceIndexedSearchParamToken resourceIndexedSearchParamToken =
1223                                createSearchParamForCoding(theResourceType, theSearchParam, theValue);
1224                if (resourceIndexedSearchParamToken != null) {
1225                        theParams.add(resourceIndexedSearchParamToken);
1226                }
1227
1228                if (shouldIndexTextComponentOfToken(theSearchParam)) {
1229                        String text = getDisplayTextForCoding(theValue);
1230                        createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text);
1231                }
1232        }
1233
1234        @Override
1235        public ResourceIndexedSearchParamToken createSearchParamForCoding(
1236                        String theResourceType, RuntimeSearchParam theSearchParam, IBase theValue) {
1237                String nextType = BaseSearchParamExtractor.this.toRootTypeName(theValue);
1238                if ("Coding".equals(nextType)) {
1239                        String system = extractValueAsString(myCodingSystemValueChild, theValue);
1240                        String code = extractValueAsString(myCodingCodeValueChild, theValue);
1241                        return createTokenIndexIfNotBlank(theResourceType, system, code, theSearchParam.getName());
1242                } else {
1243                        return null;
1244                }
1245        }
1246
1247        @Override
1248        public String getDisplayTextForCoding(IBase theValue) {
1249                String nextType = BaseSearchParamExtractor.this.toRootTypeName(theValue);
1250                if ("Coding".equals(nextType)) {
1251                        return extractValueAsString(myCodingDisplayValueChild, theValue);
1252                } else {
1253                        return null;
1254                }
1255        }
1256
1257        private void addToken_ContactPoint(
1258                        String theResourceType,
1259                        Set<BaseResourceIndexedSearchParam> theParams,
1260                        RuntimeSearchParam theSearchParam,
1261                        IBase theValue) {
1262                String system = extractValueAsString(myContactPointSystemValueChild, theValue);
1263                String value = extractValueAsString(myContactPointValueValueChild, theValue);
1264                createTokenIndexIfNotBlankAndAdd(theResourceType, theParams, theSearchParam, system, value);
1265        }
1266
1267        private void addToken_CodeableReference(
1268                        String theResourceType,
1269                        Set<BaseResourceIndexedSearchParam> theParams,
1270                        RuntimeSearchParam theSearchParam,
1271                        IBase theValue) {
1272                Optional<IBase> conceptOpt = myCodeableReferenceConcept.getAccessor().getFirstValueOrNull(theValue);
1273                conceptOpt.ifPresent(concept -> addToken_CodeableConcept(theResourceType, theParams, theSearchParam, concept));
1274        }
1275
1276        private void addToken_PatientCommunication(
1277                        String theResourceType,
1278                        Set<BaseResourceIndexedSearchParam> theParams,
1279                        RuntimeSearchParam theSearchParam,
1280                        IBase theValue) {
1281                List<IBase> values =
1282                                myPatientCommunicationLanguageValueChild.getAccessor().getValues(theValue);
1283                for (IBase next : values) {
1284                        addToken_CodeableConcept(theResourceType, theParams, theSearchParam, next);
1285                }
1286        }
1287
1288        private void addToken_CapabilityStatementRestSecurity(
1289                        String theResourceType,
1290                        Set<BaseResourceIndexedSearchParam> theParams,
1291                        RuntimeSearchParam theSearchParam,
1292                        IBase theValue) {
1293                List<IBase> values =
1294                                myCapabilityStatementRestSecurityServiceValueChild.getAccessor().getValues(theValue);
1295                for (IBase nextValue : values) {
1296                        addToken_CodeableConcept(theResourceType, theParams, theSearchParam, nextValue);
1297                }
1298        }
1299
1300        private void addDate_Period(
1301                        String theResourceType,
1302                        Set<ResourceIndexedSearchParamDate> theParams,
1303                        RuntimeSearchParam theSearchParam,
1304                        IBase theValue) {
1305                Date start = extractValueAsDate(myPeriodStartValueChild, theValue);
1306                String startAsString = extractValueAsString(myPeriodStartValueChild, theValue);
1307                Date end = extractValueAsDate(myPeriodEndValueChild, theValue);
1308                String endAsString = extractValueAsString(myPeriodEndValueChild, theValue);
1309
1310                if (start != null || end != null) {
1311
1312                        if (start == null) {
1313                                start = myStorageSettings.getPeriodIndexStartOfTime().getValue();
1314                                startAsString = myStorageSettings.getPeriodIndexStartOfTime().getValueAsString();
1315                        }
1316                        if (end == null) {
1317                                end = myStorageSettings.getPeriodIndexEndOfTime().getValue();
1318                                endAsString = myStorageSettings.getPeriodIndexEndOfTime().getValueAsString();
1319                        }
1320
1321                        ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(
1322                                        myPartitionSettings,
1323                                        theResourceType,
1324                                        theSearchParam.getName(),
1325                                        start,
1326                                        startAsString,
1327                                        end,
1328                                        endAsString,
1329                                        startAsString);
1330                        theParams.add(nextEntity);
1331                }
1332        }
1333
1334        private void addDate_Timing(
1335                        String theResourceType,
1336                        Set<ResourceIndexedSearchParamDate> theParams,
1337                        RuntimeSearchParam theSearchParam,
1338                        IBase theValue) {
1339                List<IPrimitiveType<Date>> values = extractValuesAsFhirDates(myTimingEventValueChild, theValue);
1340
1341                TreeSet<Date> dates = new TreeSet<>();
1342                String firstValue = null;
1343                String finalValue = null;
1344                for (IPrimitiveType<Date> nextEvent : values) {
1345                        if (nextEvent.getValue() != null) {
1346                                dates.add(nextEvent.getValue());
1347                                if (firstValue == null) {
1348                                        firstValue = nextEvent.getValueAsString();
1349                                }
1350                                finalValue = nextEvent.getValueAsString();
1351                        }
1352                }
1353
1354                Optional<IBase> repeat = myTimingRepeatValueChild.getAccessor().getFirstValueOrNull(theValue);
1355                if (repeat.isPresent()) {
1356                        Optional<IBase> bounds =
1357                                        myTimingRepeatBoundsValueChild.getAccessor().getFirstValueOrNull(repeat.get());
1358                        if (bounds.isPresent()) {
1359                                String boundsType = toRootTypeName(bounds.get());
1360                                if ("Period".equals(boundsType)) {
1361                                        Date start = extractValueAsDate(myPeriodStartValueChild, bounds.get());
1362                                        Date end = extractValueAsDate(myPeriodEndValueChild, bounds.get());
1363                                        String endString = extractValueAsString(myPeriodEndValueChild, bounds.get());
1364                                        dates.add(start);
1365                                        dates.add(end);
1366                                        // TODO Check if this logic is valid. Does the start of the first period indicate a lower bound??
1367                                        if (firstValue == null) {
1368                                                firstValue = extractValueAsString(myPeriodStartValueChild, bounds.get());
1369                                        }
1370                                        finalValue = endString;
1371                                }
1372                        }
1373                }
1374
1375                if (!dates.isEmpty()) {
1376                        ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(
1377                                        myPartitionSettings,
1378                                        theResourceType,
1379                                        theSearchParam.getName(),
1380                                        dates.first(),
1381                                        firstValue,
1382                                        dates.last(),
1383                                        finalValue,
1384                                        firstValue);
1385                        theParams.add(nextEntity);
1386                }
1387        }
1388
1389        private void addNumber_Duration(
1390                        String theResourceType,
1391                        Set<ResourceIndexedSearchParamNumber> theParams,
1392                        RuntimeSearchParam theSearchParam,
1393                        IBase theValue) {
1394                String system = extractValueAsString(myDurationSystemValueChild, theValue);
1395                String code = extractValueAsString(myDurationCodeValueChild, theValue);
1396                BigDecimal value = extractValueAsBigDecimal(myDurationValueValueChild, theValue);
1397                if (value != null) {
1398                        value = normalizeQuantityContainingTimeUnitsIntoDaysForNumberParam(system, code, value);
1399                        ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(
1400                                        myPartitionSettings, theResourceType, theSearchParam.getName(), value);
1401                        theParams.add(nextEntity);
1402                }
1403        }
1404
1405        private void addNumber_Quantity(
1406                        String theResourceType,
1407                        Set<ResourceIndexedSearchParamNumber> theParams,
1408                        RuntimeSearchParam theSearchParam,
1409                        IBase theValue) {
1410                BigDecimal value = extractValueAsBigDecimal(myQuantityValueValueChild, theValue);
1411                if (value != null) {
1412                        String system = extractValueAsString(myQuantitySystemValueChild, theValue);
1413                        String code = extractValueAsString(myQuantityCodeValueChild, theValue);
1414                        value = normalizeQuantityContainingTimeUnitsIntoDaysForNumberParam(system, code, value);
1415                        ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(
1416                                        myPartitionSettings, theResourceType, theSearchParam.getName(), value);
1417                        theParams.add(nextEntity);
1418                }
1419        }
1420
1421        private void addNumber_Range(
1422                        String theResourceType,
1423                        Set<ResourceIndexedSearchParamNumber> theParams,
1424                        RuntimeSearchParam theSearchParam,
1425                        IBase theValue) {
1426                Optional<IBase> low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
1427                low.ifPresent(value -> addNumber_Quantity(theResourceType, theParams, theSearchParam, value));
1428
1429                Optional<IBase> high = myRangeHighValueChild.getAccessor().getFirstValueOrNull(theValue);
1430                high.ifPresent(value -> addNumber_Quantity(theResourceType, theParams, theSearchParam, value));
1431        }
1432
1433        @SuppressWarnings("unchecked")
1434        private void addNumber_Integer(
1435                        String theResourceType,
1436                        Set<ResourceIndexedSearchParamNumber> theParams,
1437                        RuntimeSearchParam theSearchParam,
1438                        IBase theValue) {
1439                IPrimitiveType<Integer> value = (IPrimitiveType<Integer>) theValue;
1440                if (value.getValue() != null) {
1441                        BigDecimal valueDecimal = new BigDecimal(value.getValue());
1442                        ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(
1443                                        myPartitionSettings, theResourceType, theSearchParam.getName(), valueDecimal);
1444                        theParams.add(nextEntity);
1445                }
1446        }
1447
1448        @SuppressWarnings("unchecked")
1449        private void addNumber_Decimal(
1450                        String theResourceType,
1451                        Set<ResourceIndexedSearchParamNumber> theParams,
1452                        RuntimeSearchParam theSearchParam,
1453                        IBase theValue) {
1454                IPrimitiveType<BigDecimal> value = (IPrimitiveType<BigDecimal>) theValue;
1455                if (value.getValue() != null) {
1456                        BigDecimal valueDecimal = value.getValue();
1457                        ResourceIndexedSearchParamNumber nextEntity = new ResourceIndexedSearchParamNumber(
1458                                        myPartitionSettings, theResourceType, theSearchParam.getName(), valueDecimal);
1459                        theParams.add(nextEntity);
1460                }
1461        }
1462
1463        private void addCoords_Position(
1464                        String theResourceType,
1465                        SearchParamSet<BaseResourceIndexedSearchParam> theParams,
1466                        RuntimeSearchParam theSearchParam,
1467                        IBase theValue) {
1468                BigDecimal latitude = null;
1469                BigDecimal longitude = null;
1470
1471                if (theValue instanceof org.hl7.fhir.dstu3.model.Location.LocationPositionComponent) {
1472                        org.hl7.fhir.dstu3.model.Location.LocationPositionComponent value =
1473                                        (org.hl7.fhir.dstu3.model.Location.LocationPositionComponent) theValue;
1474                        latitude = value.getLatitude();
1475                        longitude = value.getLongitude();
1476                } else if (theValue instanceof org.hl7.fhir.r4.model.Location.LocationPositionComponent) {
1477                        org.hl7.fhir.r4.model.Location.LocationPositionComponent value =
1478                                        (org.hl7.fhir.r4.model.Location.LocationPositionComponent) theValue;
1479                        latitude = value.getLatitude();
1480                        longitude = value.getLongitude();
1481                } else if (theValue instanceof org.hl7.fhir.r5.model.Location.LocationPositionComponent) {
1482                        org.hl7.fhir.r5.model.Location.LocationPositionComponent value =
1483                                        (org.hl7.fhir.r5.model.Location.LocationPositionComponent) theValue;
1484                        latitude = value.getLatitude();
1485                        longitude = value.getLongitude();
1486                }
1487                // We only accept coordinates when both are present
1488                if (latitude != null && longitude != null) {
1489                        double normalizedLatitude = GeopointNormalizer.normalizeLatitude(latitude.doubleValue());
1490                        double normalizedLongitude = GeopointNormalizer.normalizeLongitude(longitude.doubleValue());
1491                        ResourceIndexedSearchParamCoords nextEntity = new ResourceIndexedSearchParamCoords(
1492                                        myPartitionSettings,
1493                                        theResourceType,
1494                                        theSearchParam.getName(),
1495                                        normalizedLatitude,
1496                                        normalizedLongitude);
1497                        theParams.add(nextEntity);
1498                }
1499        }
1500
1501        private void addString_HumanName(
1502                        String theResourceType,
1503                        Set<ResourceIndexedSearchParamString> theParams,
1504                        RuntimeSearchParam theSearchParam,
1505                        IBase theValue) {
1506                List<BaseRuntimeChildDefinition> myHumanNameChildren = Arrays.asList(
1507                                myHumanNameFamilyValueChild,
1508                                myHumanNameGivenValueChild,
1509                                myHumanNameTextValueChild,
1510                                myHumanNamePrefixValueChild,
1511                                myHumanNameSuffixValueChild);
1512                for (BaseRuntimeChildDefinition theChild : myHumanNameChildren) {
1513                        List<String> indices = extractValuesAsStrings(theChild, theValue);
1514                        for (String next : indices) {
1515                                createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, next);
1516                        }
1517                }
1518        }
1519
1520        private void addString_Quantity(
1521                        String theResourceType,
1522                        Set<ResourceIndexedSearchParamString> theParams,
1523                        RuntimeSearchParam theSearchParam,
1524                        IBase theValue) {
1525                BigDecimal value = extractValueAsBigDecimal(myQuantityValueValueChild, theValue);
1526                if (value != null) {
1527                        createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, value.toPlainString());
1528                }
1529        }
1530
1531        private void addString_Range(
1532                        String theResourceType,
1533                        Set<ResourceIndexedSearchParamString> theParams,
1534                        RuntimeSearchParam theSearchParam,
1535                        IBase theValue) {
1536                Optional<IBase> value = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
1537                value.ifPresent(t -> addString_Quantity(theResourceType, theParams, theSearchParam, t));
1538        }
1539
1540        private void addString_ContactPoint(
1541                        String theResourceType,
1542                        Set<ResourceIndexedSearchParamString> theParams,
1543                        RuntimeSearchParam theSearchParam,
1544                        IBase theValue) {
1545
1546                String value = extractValueAsString(myContactPointValueValueChild, theValue);
1547                if (isNotBlank(value)) {
1548                        createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, value);
1549                }
1550        }
1551
1552        private void addString_Address(
1553                        String theResourceType,
1554                        Set<ResourceIndexedSearchParamString> theParams,
1555                        RuntimeSearchParam theSearchParam,
1556                        IBase theValue) {
1557
1558                List<String> allNames = new ArrayList<>(extractValuesAsStrings(myAddressLineValueChild, theValue));
1559
1560                String city = extractValueAsString(myAddressCityValueChild, theValue);
1561                if (isNotBlank(city)) {
1562                        allNames.add(city);
1563                }
1564
1565                String district = extractValueAsString(myAddressDistrictValueChild, theValue);
1566                if (isNotBlank(district)) {
1567                        allNames.add(district);
1568                }
1569
1570                String state = extractValueAsString(myAddressStateValueChild, theValue);
1571                if (isNotBlank(state)) {
1572                        allNames.add(state);
1573                }
1574
1575                String country = extractValueAsString(myAddressCountryValueChild, theValue);
1576                if (isNotBlank(country)) {
1577                        allNames.add(country);
1578                }
1579
1580                String postalCode = extractValueAsString(myAddressPostalCodeValueChild, theValue);
1581                if (isNotBlank(postalCode)) {
1582                        allNames.add(postalCode);
1583                }
1584
1585                for (String nextName : allNames) {
1586                        createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, nextName);
1587                }
1588        }
1589
1590        /**
1591         * Ignore any of the Resource-level search params. This is kind of awkward, but here is why
1592         * we do it:
1593         * <p>
1594         * The ReadOnlySearchParamCache supplies these params, and they have paths associated with
1595         * them. E.g. HAPI's SearchParamRegistryImpl will know about the _id search parameter and
1596         * assigns it the path "Resource.id". All of these parameters have indexing code paths in the
1597         * server that don't rely on the existence of the SearchParameter. For example, we have a
1598         * dedicated column on ResourceTable that handles the _id parameter.
1599         * <p>
1600         * Until 6.2.0 the FhirPath evaluator didn't actually resolve any values for these paths
1601         * that started with Resource instead of the actual resource name, so it never actually
1602         * made a difference that these parameters existed because they'd never actually result
1603         * in any index rows. In 6.4.0 that bug was fixed in the core FhirPath engine. We don't
1604         * want that fix to result in pointless index rows for things like _id and _tag, so we
1605         * ignore them here.
1606         * <p>
1607         * Note that you can still create a search parameter that includes a path like
1608         * "meta.tag" if you really need to create an SP that actually does index _tag. This
1609         * is needed if you want to search for tags in <code>INLINE</code> tag storage mode.
1610         * This is the only way you could actually specify a FhirPath expression for those
1611         * prior to 6.2.0 so this isn't a breaking change.
1612         */
1613        <T> SearchParamSet<T> extractSearchParams(
1614                        IBaseResource theResource,
1615                        IExtractor<T> theExtractor,
1616                        RestSearchParameterTypeEnum theSearchParamType,
1617                        boolean theWantLocalReferences,
1618                        ISearchParamFilter theSearchParamFilter) {
1619                SearchParamSet<T> retVal = new SearchParamSet<>();
1620
1621                Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
1622
1623                int preFilterSize = searchParams.size();
1624                Collection<RuntimeSearchParam> filteredSearchParams = theSearchParamFilter.filterSearchParams(searchParams);
1625                assert filteredSearchParams.size() == preFilterSize || searchParams != filteredSearchParams;
1626
1627                cleanUpContainedResourceReferences(theResource, theSearchParamType, filteredSearchParams);
1628
1629                for (RuntimeSearchParam nextSpDef : filteredSearchParams) {
1630                        if (nextSpDef.getParamType() != theSearchParamType) {
1631                                continue;
1632                        }
1633
1634                        // See the method javadoc for an explanation of this
1635                        if (!myExtractResourceLevelParams && RuntimeSearchParamHelper.isResourceLevel(nextSpDef)) {
1636                                continue;
1637                        }
1638
1639                        extractSearchParam(nextSpDef, theResource, theExtractor, retVal, theWantLocalReferences);
1640                }
1641                return retVal;
1642        }
1643
1644        /**
1645         * Helper function to determine if a set of SPs for a resource uses a resolve as part of its fhir path.
1646         */
1647        private boolean anySearchParameterUsesResolve(
1648                        Collection<RuntimeSearchParam> searchParams, RestSearchParameterTypeEnum theSearchParamType) {
1649                return searchParams.stream()
1650                                .filter(param -> param.getParamType() != theSearchParamType)
1651                                .map(RuntimeSearchParam::getPath)
1652                                .filter(Objects::nonNull)
1653                                .anyMatch(path -> path.contains("resolve"));
1654        }
1655
1656        /**
1657         * HAPI FHIR Reference objects (e.g. {@link org.hl7.fhir.r4.model.Reference}) can hold references either by text
1658         * (e.g. "#3") or by resource (e.g. "new Reference(patientInstance)"). The FHIRPath evaluator only understands the
1659         * first way, so if there is any chance of the FHIRPath evaluator needing to descend across references, we
1660         * have to assign values to those references before indexing.
1661         * <p>
1662         * Doing this cleanup isn't hugely expensive, but it's not completely free either so we only do it
1663         * if we think there's actually a chance
1664         */
1665        private void cleanUpContainedResourceReferences(
1666                        IBaseResource theResource,
1667                        RestSearchParameterTypeEnum theSearchParamType,
1668                        Collection<RuntimeSearchParam> searchParams) {
1669                boolean havePathWithResolveExpression = myStorageSettings.isIndexOnContainedResources()
1670                                || anySearchParameterUsesResolve(searchParams, theSearchParamType);
1671
1672                if (havePathWithResolveExpression && myContext.getParserOptions().isAutoContainReferenceTargetsWithNoId()) {
1673                        // TODO GGG/JA: At this point, if the Task.basedOn.reference.resource does _not_ have an ID, we will attempt
1674                        // to contain it internally. Wild
1675                        myContext
1676                                        .newTerser()
1677                                        .containResources(
1678                                                        theResource,
1679                                                        FhirTerser.OptionsEnum.MODIFY_RESOURCE,
1680                                                        FhirTerser.OptionsEnum.STORE_AND_REUSE_RESULTS);
1681                }
1682        }
1683
1684        /**
1685         * extract for normal SP
1686         */
1687        @VisibleForTesting
1688        public <T> void extractSearchParam(
1689                        RuntimeSearchParam theSearchParameterDef,
1690                        IBase theResource,
1691                        IExtractor<T> theExtractor,
1692                        SearchParamSet<T> theSetToPopulate,
1693                        boolean theWantLocalReferences) {
1694                String nextPathUnsplit = theSearchParameterDef.getPath();
1695                extractSearchParam(
1696                                theSearchParameterDef,
1697                                nextPathUnsplit,
1698                                theResource,
1699                                theExtractor,
1700                                theSetToPopulate,
1701                                theWantLocalReferences);
1702        }
1703
1704        /**
1705         * extract for SP, but with possibly different expression.
1706         * Allows composite SPs to use sub-paths.
1707         */
1708        private <T> void extractSearchParam(
1709                        RuntimeSearchParam theSearchParameterDef,
1710                        String thePathExpression,
1711                        IBase theResource,
1712                        IExtractor<T> theExtractor,
1713                        SearchParamSet<T> theSetToPopulate,
1714                        boolean theWantLocalReferences) {
1715                if (isBlank(thePathExpression)) {
1716                        return;
1717                }
1718
1719                String[] splitPaths = split(thePathExpression);
1720                for (String nextPath : splitPaths) {
1721                        nextPath = trim(nextPath);
1722                        for (IBase nextObject : extractValues(nextPath, theResource)) {
1723                                if (nextObject != null) {
1724                                        String typeName = toRootTypeName(nextObject);
1725                                        if (!myIgnoredForSearchDatatypes.contains(typeName)) {
1726                                                theExtractor.extract(
1727                                                                theSetToPopulate, theSearchParameterDef, nextObject, nextPath, theWantLocalReferences);
1728                                        }
1729                                }
1730                        }
1731                }
1732        }
1733
1734        @Override
1735        public String toRootTypeName(IBase nextObject) {
1736                BaseRuntimeElementDefinition<?> elementDefinition = getContext().getElementDefinition(nextObject.getClass());
1737                BaseRuntimeElementDefinition<?> rootParentDefinition = elementDefinition.getRootParentDefinition();
1738                return rootParentDefinition.getName();
1739        }
1740
1741        @Override
1742        public String toTypeName(IBase nextObject) {
1743                BaseRuntimeElementDefinition<?> elementDefinition = getContext().getElementDefinition(nextObject.getClass());
1744                return elementDefinition.getName();
1745        }
1746
1747        private void addUri_Uri(
1748                        String theResourceType,
1749                        Set<ResourceIndexedSearchParamUri> theParams,
1750                        RuntimeSearchParam theSearchParam,
1751                        IBase theValue) {
1752                IPrimitiveType<?> value = (IPrimitiveType<?>) theValue;
1753                String valueAsString = value.getValueAsString();
1754                if (isNotBlank(valueAsString)) {
1755                        ResourceIndexedSearchParamUri nextEntity = new ResourceIndexedSearchParamUri(
1756                                        myPartitionSettings, theResourceType, theSearchParam.getName(), valueAsString);
1757                        theParams.add(nextEntity);
1758                }
1759        }
1760
1761        @SuppressWarnings({"UnnecessaryLocalVariable"})
1762        private void createStringIndexIfNotBlank(
1763                        String theResourceType,
1764                        Set<? extends BaseResourceIndexedSearchParam> theParams,
1765                        RuntimeSearchParam theSearchParam,
1766                        String theValue) {
1767                String value = theValue;
1768                if (isNotBlank(value)) {
1769                        if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
1770                                value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH);
1771                        }
1772
1773                        String searchParamName = theSearchParam.getName();
1774                        String valueNormalized = StringUtil.normalizeStringForSearchIndexing(value);
1775                        String valueEncoded = theSearchParam.encode(valueNormalized);
1776
1777                        if (valueEncoded.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
1778                                valueEncoded = valueEncoded.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH);
1779                        }
1780
1781                        ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(
1782                                        myPartitionSettings, getStorageSettings(), theResourceType, searchParamName, valueEncoded, value);
1783
1784                        Set params = theParams;
1785                        params.add(nextEntity);
1786                }
1787        }
1788
1789        private void createTokenIndexIfNotBlankAndAdd(
1790                        String theResourceType,
1791                        Set<BaseResourceIndexedSearchParam> theParams,
1792                        RuntimeSearchParam theSearchParam,
1793                        String theSystem,
1794                        String theValue) {
1795                ResourceIndexedSearchParamToken nextEntity =
1796                                createTokenIndexIfNotBlank(theResourceType, theSystem, theValue, theSearchParam.getName());
1797                if (nextEntity != null) {
1798                        theParams.add(nextEntity);
1799                }
1800        }
1801
1802        @VisibleForTesting
1803        public void setPartitionSettings(PartitionSettings thePartitionSettings) {
1804                myPartitionSettings = thePartitionSettings;
1805        }
1806
1807        private ResourceIndexedSearchParamToken createTokenIndexIfNotBlank(
1808                        String theResourceType, String theSystem, String theValue, String searchParamName) {
1809                ResourceIndexedSearchParamToken nextEntity = null;
1810                if (isNotBlank(theSystem) || isNotBlank(theValue)) {
1811                        nextEntity = new ResourceIndexedSearchParamToken(
1812                                        myPartitionSettings, theResourceType, searchParamName, theSystem, theValue);
1813                }
1814                return nextEntity;
1815        }
1816
1817        @Override
1818        public String[] split(String thePaths) {
1819                if (shouldAttemptToSplitPath(thePaths)) {
1820                        return splitOutOfParensOrs(thePaths);
1821                } else {
1822                        return new String[] {thePaths};
1823                }
1824        }
1825
1826        public boolean shouldAttemptToSplitPath(String thePath) {
1827                if (getContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
1828                        return thePath.contains("|");
1829                } else {
1830                        // DSTU 3 and below used "or" as well as "|"
1831                        return thePath.contains("|") || thePath.contains(" or ");
1832                }
1833        }
1834
1835        /**
1836         * Iteratively splits a string on any ` or ` or | that is ** not** contained inside a set of parentheses. e.g.
1837         * <p>
1838         * "Patient.select(a or b)" -->  ["Patient.select(a or b)"]
1839         * "Patient.select(a or b) or Patient.select(c or d )" --> ["Patient.select(a or b)", "Patient.select(c or d)"]
1840         * "Patient.select(a|b) or Patient.select(c or d )" --> ["Patient.select(a|b)", "Patient.select(c or d)"]
1841         * "Patient.select(b) | Patient.select(c)" -->  ["Patient.select(b)", "Patient.select(c)"]
1842         *
1843         * @param thePaths The string to split
1844         * @return The split string
1845         */
1846        private String[] splitOutOfParensOrs(String thePaths) {
1847                List<String> topLevelOrExpressions = splitOutOfParensToken(thePaths, " or ");
1848                return topLevelOrExpressions.stream()
1849                                .flatMap(s -> splitOutOfParensToken(s, " |").stream())
1850                                .toArray(String[]::new);
1851        }
1852
1853        private List<String> splitOutOfParensToken(String thePath, String theToken) {
1854                int tokenLength = theToken.length();
1855                int index = thePath.indexOf(theToken);
1856                int rightIndex = 0;
1857                List<String> retVal = new ArrayList<>();
1858                while (index > -1) {
1859                        String left = thePath.substring(rightIndex, index);
1860                        if (allParensHaveBeenClosed(left)) {
1861                                retVal.add(left);
1862                                rightIndex = index + tokenLength;
1863                        }
1864                        index = thePath.indexOf(theToken, index + tokenLength);
1865                }
1866                retVal.add(thePath.substring(rightIndex));
1867                return retVal;
1868        }
1869
1870        private boolean allParensHaveBeenClosed(String thePaths) {
1871                int open = StringUtils.countMatches(thePaths, "(");
1872                int close = StringUtils.countMatches(thePaths, ")");
1873                return open == close;
1874        }
1875
1876        private BigDecimal normalizeQuantityContainingTimeUnitsIntoDaysForNumberParam(
1877                        String theSystem, String theCode, BigDecimal theValue) {
1878                if (SearchParamConstants.UCUM_NS.equals(theSystem)) {
1879                        if (isNotBlank(theCode)) {
1880                                Unit<? extends Quantity> unit = Unit.valueOf(theCode);
1881                                javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY);
1882                                double dayValue = dayConverter.convert(theValue.doubleValue());
1883                                theValue = new BigDecimal(dayValue);
1884                        }
1885                }
1886                return theValue;
1887        }
1888
1889        @PostConstruct
1890        public void start() {
1891                myIgnoredForSearchDatatypes = new HashSet<>();
1892                addIgnoredType(getContext(), "Annotation", myIgnoredForSearchDatatypes);
1893                addIgnoredType(getContext(), "Attachment", myIgnoredForSearchDatatypes);
1894                addIgnoredType(getContext(), "Count", myIgnoredForSearchDatatypes);
1895                addIgnoredType(getContext(), "Distance", myIgnoredForSearchDatatypes);
1896                addIgnoredType(getContext(), "Ratio", myIgnoredForSearchDatatypes);
1897                addIgnoredType(getContext(), "SampledData", myIgnoredForSearchDatatypes);
1898                addIgnoredType(getContext(), "Signature", myIgnoredForSearchDatatypes);
1899
1900                /*
1901                 * This is building up an internal map of all the various field accessors we'll need in order to work
1902                 * with the model. This is kind of ugly, but we want to be as efficient as possible since
1903                 * search param extraction happens a whole heck of a lot at runtime..
1904                 */
1905
1906                BaseRuntimeElementCompositeDefinition<?> quantityDefinition =
1907                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Quantity");
1908                myQuantityValueValueChild = quantityDefinition.getChildByName("value");
1909                myQuantitySystemValueChild = quantityDefinition.getChildByName("system");
1910                myQuantityCodeValueChild = quantityDefinition.getChildByName("code");
1911
1912                BaseRuntimeElementCompositeDefinition<?> moneyDefinition =
1913                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Money");
1914                myMoneyValueChild = moneyDefinition.getChildByName("value");
1915                myMoneyCurrencyChild = moneyDefinition.getChildByName("currency");
1916
1917                BaseRuntimeElementCompositeDefinition<?> locationDefinition =
1918                                getContext().getResourceDefinition("Location");
1919                BaseRuntimeChildDefinition locationPositionValueChild = locationDefinition.getChildByName("position");
1920                myLocationPositionDefinition =
1921                                (BaseRuntimeElementCompositeDefinition<?>) locationPositionValueChild.getChildByName("position");
1922
1923                BaseRuntimeElementCompositeDefinition<?> rangeDefinition =
1924                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Range");
1925                myRangeLowValueChild = rangeDefinition.getChildByName("low");
1926                myRangeHighValueChild = rangeDefinition.getChildByName("high");
1927
1928                BaseRuntimeElementCompositeDefinition<?> addressDefinition =
1929                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Address");
1930                myAddressLineValueChild = addressDefinition.getChildByName("line");
1931                myAddressCityValueChild = addressDefinition.getChildByName("city");
1932                myAddressDistrictValueChild = addressDefinition.getChildByName("district");
1933                myAddressStateValueChild = addressDefinition.getChildByName("state");
1934                myAddressCountryValueChild = addressDefinition.getChildByName("country");
1935                myAddressPostalCodeValueChild = addressDefinition.getChildByName("postalCode");
1936
1937                BaseRuntimeElementCompositeDefinition<?> periodDefinition =
1938                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Period");
1939                myPeriodStartValueChild = periodDefinition.getChildByName("start");
1940                myPeriodEndValueChild = periodDefinition.getChildByName("end");
1941
1942                BaseRuntimeElementCompositeDefinition<?> timingDefinition =
1943                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Timing");
1944                myTimingEventValueChild = timingDefinition.getChildByName("event");
1945                myTimingRepeatValueChild = timingDefinition.getChildByName("repeat");
1946                BaseRuntimeElementCompositeDefinition<?> timingRepeatDefinition =
1947                                (BaseRuntimeElementCompositeDefinition<?>) myTimingRepeatValueChild.getChildByName("repeat");
1948                myTimingRepeatBoundsValueChild = timingRepeatDefinition.getChildByName("bounds[x]");
1949
1950                BaseRuntimeElementCompositeDefinition<?> durationDefinition =
1951                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Duration");
1952                myDurationSystemValueChild = durationDefinition.getChildByName("system");
1953                myDurationCodeValueChild = durationDefinition.getChildByName("code");
1954                myDurationValueValueChild = durationDefinition.getChildByName("value");
1955
1956                BaseRuntimeElementCompositeDefinition<?> humanNameDefinition =
1957                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("HumanName");
1958                myHumanNameFamilyValueChild = humanNameDefinition.getChildByName("family");
1959                myHumanNameGivenValueChild = humanNameDefinition.getChildByName("given");
1960                myHumanNameTextValueChild = humanNameDefinition.getChildByName("text");
1961                myHumanNamePrefixValueChild = humanNameDefinition.getChildByName("prefix");
1962                myHumanNameSuffixValueChild = humanNameDefinition.getChildByName("suffix");
1963
1964                BaseRuntimeElementCompositeDefinition<?> contactPointDefinition =
1965                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("ContactPoint");
1966                myContactPointValueValueChild = contactPointDefinition.getChildByName("value");
1967                myContactPointSystemValueChild = contactPointDefinition.getChildByName("system");
1968
1969                BaseRuntimeElementCompositeDefinition<?> identifierDefinition =
1970                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Identifier");
1971                myIdentifierSystemValueChild = identifierDefinition.getChildByName("system");
1972                myIdentifierValueValueChild = identifierDefinition.getChildByName("value");
1973                myIdentifierTypeValueChild = identifierDefinition.getChildByName("type");
1974                BaseRuntimeElementCompositeDefinition<?> identifierTypeDefinition =
1975                                (BaseRuntimeElementCompositeDefinition<?>) myIdentifierTypeValueChild.getChildByName("type");
1976                myIdentifierTypeTextValueChild = identifierTypeDefinition.getChildByName("text");
1977
1978                BaseRuntimeElementCompositeDefinition<?> codeableConceptDefinition =
1979                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("CodeableConcept");
1980                myCodeableConceptCodingValueChild = codeableConceptDefinition.getChildByName("coding");
1981                myCodeableConceptTextValueChild = codeableConceptDefinition.getChildByName("text");
1982
1983                BaseRuntimeElementCompositeDefinition<?> codingDefinition =
1984                                (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("Coding");
1985                myCodingSystemValueChild = codingDefinition.getChildByName("system");
1986                myCodingCodeValueChild = codingDefinition.getChildByName("code");
1987                myCodingDisplayValueChild = codingDefinition.getChildByName("display");
1988
1989                BaseRuntimeElementCompositeDefinition<?> patientDefinition =
1990                                getContext().getResourceDefinition("Patient");
1991                BaseRuntimeChildDefinition patientCommunicationValueChild = patientDefinition.getChildByName("communication");
1992                BaseRuntimeElementCompositeDefinition<?> patientCommunicationDefinition =
1993                                (BaseRuntimeElementCompositeDefinition<?>)
1994                                                patientCommunicationValueChild.getChildByName("communication");
1995                myPatientCommunicationLanguageValueChild = patientCommunicationDefinition.getChildByName("language");
1996
1997                // DSTU3+
1998                BaseRuntimeElementCompositeDefinition<?> codeSystemDefinition;
1999                if (getContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
2000                        codeSystemDefinition = getContext().getResourceDefinition("CodeSystem");
2001                        assert codeSystemDefinition != null;
2002                        myCodeSystemUrlValueChild = codeSystemDefinition.getChildByName("url");
2003
2004                        BaseRuntimeElementCompositeDefinition<?> capabilityStatementDefinition =
2005                                        getContext().getResourceDefinition("CapabilityStatement");
2006                        BaseRuntimeChildDefinition capabilityStatementRestChild =
2007                                        capabilityStatementDefinition.getChildByName("rest");
2008                        BaseRuntimeElementCompositeDefinition<?> capabilityStatementRestDefinition =
2009                                        (BaseRuntimeElementCompositeDefinition<?>) capabilityStatementRestChild.getChildByName("rest");
2010                        BaseRuntimeChildDefinition capabilityStatementRestSecurityValueChild =
2011                                        capabilityStatementRestDefinition.getChildByName("security");
2012                        BaseRuntimeElementCompositeDefinition<?> capabilityStatementRestSecurityDefinition =
2013                                        (BaseRuntimeElementCompositeDefinition<?>)
2014                                                        capabilityStatementRestSecurityValueChild.getChildByName("security");
2015                        myCapabilityStatementRestSecurityServiceValueChild =
2016                                        capabilityStatementRestSecurityDefinition.getChildByName("service");
2017                }
2018
2019                // R4B+
2020                if (getContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4B)) {
2021
2022                        BaseRuntimeElementCompositeDefinition<?> codeableReferenceDef =
2023                                        (BaseRuntimeElementCompositeDefinition<?>) getContext().getElementDefinition("CodeableReference");
2024                        myCodeableReferenceConcept = codeableReferenceDef.getChildByName("concept");
2025                        myCodeableReferenceReference = codeableReferenceDef.getChildByName("reference");
2026                }
2027        }
2028
2029        @FunctionalInterface
2030        public interface IValueExtractor {
2031
2032                List<? extends IBase> get() throws FHIRException;
2033        }
2034
2035        @VisibleForTesting
2036        @FunctionalInterface
2037        interface IExtractor<T> {
2038
2039                void extract(
2040                                SearchParamSet<T> theParams,
2041                                RuntimeSearchParam theSearchParam,
2042                                IBase theValue,
2043                                String thePath,
2044                                boolean theWantLocalReferences);
2045        }
2046
2047        /**
2048         * Note that this should only be called for R4+ servers. Prior to
2049         * R4 the paths could be separated by the word "or" or by a "|"
2050         * character, so we used a slower splitting mechanism.
2051         */
2052        @Nonnull
2053        public static String[] splitPathsR4(@Nonnull String thePaths) {
2054                StringTokenizer tok = new StringTokenizer(thePaths, " |");
2055                tok.setTrimmerMatcher(new StringTrimmingTrimmerMatcher());
2056                return tok.getTokenArray();
2057        }
2058
2059        public static boolean tokenTextIndexingEnabledForSearchParam(
2060                        StorageSettings theStorageSettings, RuntimeSearchParam theSearchParam) {
2061                Optional<Boolean> noSuppressForSearchParam =
2062                                theSearchParam.getExtensions(HapiExtensions.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING).stream()
2063                                                .map(IBaseExtension::getValue)
2064                                                .map(val -> (IPrimitiveType<?>) val)
2065                                                .map(IPrimitiveType::getValueAsString)
2066                                                .map(Boolean::parseBoolean)
2067                                                .findFirst();
2068
2069                // if the SP doesn't care, use the system default.
2070                if (noSuppressForSearchParam.isEmpty()) {
2071                        return !theStorageSettings.isSuppressStringIndexingInTokens();
2072                        // If the SP does care, use its value.
2073                } else {
2074                        boolean suppressForSearchParam = noSuppressForSearchParam.get();
2075                        ourLog.trace("Text indexing for SearchParameter {}: {}", theSearchParam.getName(), suppressForSearchParam);
2076                        return !suppressForSearchParam;
2077                }
2078        }
2079
2080        private static void addIgnoredType(FhirContext theCtx, String theType, Set<String> theIgnoredTypes) {
2081                BaseRuntimeElementDefinition<?> elementDefinition = theCtx.getElementDefinition(theType);
2082                if (elementDefinition != null) {
2083                        theIgnoredTypes.add(elementDefinition.getName());
2084                }
2085        }
2086
2087        protected static String extractValueAsString(BaseRuntimeChildDefinition theChildDefinition, IBase theElement) {
2088                return theChildDefinition
2089                                .getAccessor()
2090                                .<IPrimitiveType<?>>getFirstValueOrNull(theElement)
2091                                .map(IPrimitiveType::getValueAsString)
2092                                .orElse(null);
2093        }
2094
2095        protected static Date extractValueAsDate(BaseRuntimeChildDefinition theChildDefinition, IBase theElement) {
2096                return theChildDefinition
2097                                .getAccessor()
2098                                .<IPrimitiveType<Date>>getFirstValueOrNull(theElement)
2099                                .map(IPrimitiveType::getValue)
2100                                .orElse(null);
2101        }
2102
2103        protected static BigDecimal extractValueAsBigDecimal(
2104                        BaseRuntimeChildDefinition theChildDefinition, IBase theElement) {
2105                return theChildDefinition
2106                                .getAccessor()
2107                                .<IPrimitiveType<BigDecimal>>getFirstValueOrNull(theElement)
2108                                .map(IPrimitiveType::getValue)
2109                                .orElse(null);
2110        }
2111
2112        @SuppressWarnings("unchecked")
2113        protected static List<IPrimitiveType<Date>> extractValuesAsFhirDates(
2114                        BaseRuntimeChildDefinition theChildDefinition, IBase theElement) {
2115                return (List) theChildDefinition.getAccessor().getValues(theElement);
2116        }
2117
2118        protected static List<String> extractValuesAsStrings(
2119                        BaseRuntimeChildDefinition theChildDefinition, IBase theValue) {
2120                return theChildDefinition.getAccessor().getValues(theValue).stream()
2121                                .map(t -> (IPrimitiveType) t)
2122                                .map(IPrimitiveType::getValueAsString)
2123                                .filter(StringUtils::isNotBlank)
2124                                .collect(Collectors.toList());
2125        }
2126
2127        protected static <T extends Enum<?>> String extractSystem(IBaseEnumeration<T> theBoundCode) {
2128                if (theBoundCode.getValue() != null) {
2129                        return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue());
2130                }
2131                return null;
2132        }
2133
2134        private class ResourceLinkExtractor implements IExtractor<PathAndRef> {
2135
2136                private PathAndRef myPathAndRef = null;
2137
2138                @Override
2139                public void extract(
2140                                SearchParamSet<PathAndRef> theParams,
2141                                RuntimeSearchParam theSearchParam,
2142                                IBase theValue,
2143                                String thePath,
2144                                boolean theWantLocalReferences) {
2145                        if (theValue instanceof IBaseResource) {
2146                                myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, (IBaseResource) theValue);
2147                                theParams.add(myPathAndRef);
2148                                return;
2149                        }
2150
2151                        String nextType = toRootTypeName(theValue);
2152                        switch (nextType) {
2153                                case "uri":
2154                                case "canonical":
2155                                        String typeName = toTypeName(theValue);
2156                                        IPrimitiveType<?> valuePrimitive = (IPrimitiveType<?>) theValue;
2157                                        IBaseReference fakeReference = (IBaseReference)
2158                                                        myContext.getElementDefinition("Reference").newInstance();
2159                                        fakeReference.setReference(valuePrimitive.getValueAsString());
2160
2161                                        // Canonical has a root type of "uri"
2162                                        if ("canonical".equals(typeName)) {
2163
2164                                                /*
2165                                                 * See #1583
2166                                                 * Technically canonical fields should not allow local references (e.g.
2167                                                 * Questionnaire/123) but it seems reasonable for us to interpret a canonical
2168                                                 * containing a local reference for what it is, and allow people to search
2169                                                 * based on that.
2170                                                 */
2171                                                IIdType parsed = fakeReference.getReferenceElement();
2172                                                if (parsed.hasIdPart() && parsed.hasResourceType() && !parsed.isAbsolute()) {
2173                                                        myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, fakeReference, false);
2174                                                        theParams.add(myPathAndRef);
2175                                                        break;
2176                                                }
2177
2178                                                if (parsed.isAbsolute()) {
2179                                                        String refValue =
2180                                                                        fakeReference.getReferenceElement().getValue();
2181
2182                                                        myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, fakeReference, true);
2183                                                        theParams.add(myPathAndRef);
2184
2185                                                        /*
2186                                                         * If we have a versioned canonical uri,
2187                                                         * we will index both the version and unversioned uri
2188                                                         * (ie: uri|version and uri)
2189                                                         * This will allow searching to work on both versioned and non-versioned.
2190                                                         *
2191                                                         * HOWEVER
2192                                                         * This doesn't actually fix chained searching (MeasureReport?measure.identifier=...)
2193                                                         */
2194                                                        if (refValue.contains("|")) {
2195                                                                // extract the non-versioned AND the versioned above so both searches work.
2196                                                                fakeReference = (IBaseReference) myContext
2197                                                                                .getElementDefinition("Reference")
2198                                                                                .newInstance();
2199                                                                fakeReference.setReference(refValue.substring(0, refValue.indexOf('|')));
2200                                                        }
2201
2202                                                        myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, fakeReference, true);
2203                                                        theParams.add(myPathAndRef);
2204                                                }
2205
2206                                                break;
2207                                        }
2208
2209                                        theParams.addWarning("Ignoring canonical reference (indexing canonical is not yet supported)");
2210                                        break;
2211                                case "reference":
2212                                case "Reference":
2213                                        IBaseReference valueRef = (IBaseReference) theValue;
2214                                        extractResourceLinkFromReference(
2215                                                        theParams, theSearchParam, thePath, theWantLocalReferences, valueRef);
2216                                        break;
2217                                case "CodeableReference":
2218                                        Optional<IBase> referenceOpt =
2219                                                        myCodeableReferenceReference.getAccessor().getFirstValueOrNull(theValue);
2220                                        if (referenceOpt.isPresent()) {
2221                                                IBaseReference value = (IBaseReference) referenceOpt.get();
2222                                                extractResourceLinkFromReference(
2223                                                                theParams, theSearchParam, thePath, theWantLocalReferences, value);
2224                                        }
2225                                        break;
2226                                default:
2227                                        addUnexpectedDatatypeWarning(theParams, theSearchParam, theValue, thePath);
2228                                        break;
2229                        }
2230                }
2231
2232                private void extractResourceLinkFromReference(
2233                                SearchParamSet<PathAndRef> theParams,
2234                                RuntimeSearchParam theSearchParam,
2235                                String thePath,
2236                                boolean theWantLocalReferences,
2237                                IBaseReference valueRef) {
2238                        IIdType nextId = valueRef.getReferenceElement();
2239                        if (nextId.isEmpty() && valueRef.getResource() != null) {
2240                                nextId = valueRef.getResource().getIdElement();
2241                        }
2242
2243                        if (nextId == null || nextId.isEmpty()) {
2244                                // Ignore placeholder references that are blank
2245                        } else if (!theWantLocalReferences && nextId.getValue().startsWith("#")) {
2246                                // Ignore local refs unless we specifically want them
2247                        } else {
2248                                myPathAndRef = new PathAndRef(theSearchParam.getName(), thePath, valueRef, false);
2249                                theParams.add(myPathAndRef);
2250                        }
2251                }
2252
2253                public PathAndRef get(IBase theValue, String thePath) {
2254                        extract(
2255                                        new SearchParamSet<>(),
2256                                        new RuntimeSearchParam(null, null, "Reference", null, null, null, null, null, null, null),
2257                                        theValue,
2258                                        thePath,
2259                                        false);
2260                        return myPathAndRef;
2261                }
2262        }
2263
2264        private class DateExtractor implements IExtractor<ResourceIndexedSearchParamDate> {
2265
2266                final String myResourceType;
2267                ResourceIndexedSearchParamDate myIndexedSearchParamDate = null;
2268
2269                public DateExtractor(IBaseResource theResource) {
2270                        this(toRootTypeName(theResource));
2271                }
2272
2273                public DateExtractor(String theResourceType) {
2274                        myResourceType = theResourceType;
2275                }
2276
2277                @Override
2278                public void extract(
2279                                SearchParamSet theParams,
2280                                RuntimeSearchParam theSearchParam,
2281                                IBase theValue,
2282                                String thePath,
2283                                boolean theWantLocalReferences) {
2284                        String nextType = toRootTypeName(theValue);
2285                        switch (nextType) {
2286                                case "date":
2287                                case "dateTime":
2288                                case "instant":
2289                                        addDateTimeTypes(myResourceType, theParams, theSearchParam, theValue);
2290                                        break;
2291                                case "Period":
2292                                        addDate_Period(myResourceType, theParams, theSearchParam, theValue);
2293                                        break;
2294                                case "Timing":
2295                                        addDate_Timing(myResourceType, theParams, theSearchParam, theValue);
2296                                        break;
2297                                case "string":
2298                                        // CarePlan.activitydate can be a string - ignored for now
2299                                        break;
2300                                case "Quantity":
2301                                        // Condition.onset[x] can have a Quantity - Ignored for now
2302                                        break;
2303                                case "Range":
2304                                        // Condition.onset[x] can have a Range - Ignored for now
2305                                        break;
2306                                default:
2307                                        addUnexpectedDatatypeWarning(theParams, theSearchParam, theValue, thePath);
2308                                        break;
2309                        }
2310                }
2311
2312                private void addDate_Period(
2313                                String theResourceType,
2314                                Set<ResourceIndexedSearchParamDate> theParams,
2315                                RuntimeSearchParam theSearchParam,
2316                                IBase theValue) {
2317                        Date start = extractValueAsDate(myPeriodStartValueChild, theValue);
2318                        String startAsString = extractValueAsString(myPeriodStartValueChild, theValue);
2319                        Date end = extractValueAsDate(myPeriodEndValueChild, theValue);
2320                        String endAsString = extractValueAsString(myPeriodEndValueChild, theValue);
2321
2322                        if (start != null || end != null) {
2323
2324                                if (start == null) {
2325                                        start = myStorageSettings.getPeriodIndexStartOfTime().getValue();
2326                                        startAsString =
2327                                                        myStorageSettings.getPeriodIndexStartOfTime().getValueAsString();
2328                                }
2329                                if (end == null) {
2330                                        end = myStorageSettings.getPeriodIndexEndOfTime().getValue();
2331                                        endAsString = myStorageSettings.getPeriodIndexEndOfTime().getValueAsString();
2332                                }
2333
2334                                myIndexedSearchParamDate = new ResourceIndexedSearchParamDate(
2335                                                myPartitionSettings,
2336                                                theResourceType,
2337                                                theSearchParam.getName(),
2338                                                start,
2339                                                startAsString,
2340                                                end,
2341                                                endAsString,
2342                                                startAsString);
2343                                theParams.add(myIndexedSearchParamDate);
2344                        }
2345                }
2346
2347                private void addDate_Timing(
2348                                String theResourceType,
2349                                Set<ResourceIndexedSearchParamDate> theParams,
2350                                RuntimeSearchParam theSearchParam,
2351                                IBase theValue) {
2352                        List<IPrimitiveType<Date>> values = extractValuesAsFhirDates(myTimingEventValueChild, theValue);
2353
2354                        TreeSet<Date> dates = new TreeSet<>();
2355                        String firstValue = null;
2356                        String finalValue = null;
2357                        for (IPrimitiveType<Date> nextEvent : values) {
2358                                if (nextEvent.getValue() != null) {
2359                                        dates.add(nextEvent.getValue());
2360                                        if (firstValue == null) {
2361                                                firstValue = nextEvent.getValueAsString();
2362                                        }
2363                                        finalValue = nextEvent.getValueAsString();
2364                                }
2365                        }
2366
2367                        Optional<IBase> repeat = myTimingRepeatValueChild.getAccessor().getFirstValueOrNull(theValue);
2368                        if (repeat.isPresent()) {
2369                                Optional<IBase> bounds =
2370                                                myTimingRepeatBoundsValueChild.getAccessor().getFirstValueOrNull(repeat.get());
2371                                if (bounds.isPresent()) {
2372                                        String boundsType = toRootTypeName(bounds.get());
2373                                        if ("Period".equals(boundsType)) {
2374                                                Date start = extractValueAsDate(myPeriodStartValueChild, bounds.get());
2375                                                Date end = extractValueAsDate(myPeriodEndValueChild, bounds.get());
2376                                                if (start != null) {
2377                                                        dates.add(start);
2378                                                }
2379                                                if (end != null) {
2380                                                        dates.add(end);
2381                                                }
2382                                        }
2383                                }
2384                        }
2385
2386                        if (!dates.isEmpty()) {
2387                                myIndexedSearchParamDate = new ResourceIndexedSearchParamDate(
2388                                                myPartitionSettings,
2389                                                theResourceType,
2390                                                theSearchParam.getName(),
2391                                                dates.first(),
2392                                                firstValue,
2393                                                dates.last(),
2394                                                finalValue,
2395                                                firstValue);
2396                                theParams.add(myIndexedSearchParamDate);
2397                        }
2398                }
2399
2400                @SuppressWarnings("unchecked")
2401                private void addDateTimeTypes(
2402                                String theResourceType,
2403                                Set<ResourceIndexedSearchParamDate> theParams,
2404                                RuntimeSearchParam theSearchParam,
2405                                IBase theValue) {
2406                        IPrimitiveType<Date> nextBaseDateTime = (IPrimitiveType<Date>) theValue;
2407                        if (nextBaseDateTime.getValue() != null) {
2408                                myIndexedSearchParamDate = new ResourceIndexedSearchParamDate(
2409                                                myPartitionSettings,
2410                                                theResourceType,
2411                                                theSearchParam.getName(),
2412                                                nextBaseDateTime.getValue(),
2413                                                nextBaseDateTime.getValueAsString(),
2414                                                nextBaseDateTime.getValue(),
2415                                                nextBaseDateTime.getValueAsString(),
2416                                                nextBaseDateTime.getValueAsString());
2417                                ourLog.trace("DateExtractor - extracted {} for {}", nextBaseDateTime, theSearchParam.getName());
2418                                theParams.add(myIndexedSearchParamDate);
2419                        }
2420                }
2421
2422                public ResourceIndexedSearchParamDate get(IBase theValue, String thePath, boolean theWantLocalReferences) {
2423                        extract(
2424                                        new SearchParamSet<>(),
2425                                        new RuntimeSearchParam(null, null, "date", null, null, null, null, null, null, null),
2426                                        theValue,
2427                                        thePath,
2428                                        theWantLocalReferences);
2429                        return myIndexedSearchParamDate;
2430                }
2431        }
2432
2433        private class TokenExtractor implements IExtractor<BaseResourceIndexedSearchParam> {
2434                private final String myResourceTypeName;
2435                private final String myUseSystem;
2436
2437                public TokenExtractor(String theResourceTypeName, String theUseSystem) {
2438                        myResourceTypeName = theResourceTypeName;
2439                        myUseSystem = theUseSystem;
2440                }
2441
2442                @Override
2443                public void extract(
2444                                SearchParamSet<BaseResourceIndexedSearchParam> params,
2445                                RuntimeSearchParam searchParam,
2446                                IBase value,
2447                                String path,
2448                                boolean theWantLocalReferences) {
2449
2450                        // DSTU3+
2451                        if (value instanceof IBaseEnumeration<?>) {
2452                                IBaseEnumeration<?> obj = (IBaseEnumeration<?>) value;
2453                                String system = extractSystem(obj);
2454                                String code = obj.getValueAsString();
2455                                BaseSearchParamExtractor.this.createTokenIndexIfNotBlankAndAdd(
2456                                                myResourceTypeName, params, searchParam, system, code);
2457                                return;
2458                        }
2459
2460                        // DSTU2 only
2461                        if (value instanceof BoundCodeDt) {
2462                                BoundCodeDt boundCode = (BoundCodeDt) value;
2463                                Enum<?> valueAsEnum = boundCode.getValueAsEnum();
2464                                String system = null;
2465                                if (valueAsEnum != null) {
2466                                        //noinspection unchecked
2467                                        system = boundCode.getBinder().toSystemString(valueAsEnum);
2468                                }
2469                                String code = boundCode.getValueAsString();
2470                                BaseSearchParamExtractor.this.createTokenIndexIfNotBlankAndAdd(
2471                                                myResourceTypeName, params, searchParam, system, code);
2472                                return;
2473                        }
2474
2475                        if (value instanceof IPrimitiveType) {
2476                                IPrimitiveType<?> nextValue = (IPrimitiveType<?>) value;
2477                                String systemAsString = null;
2478                                String valueAsString = nextValue.getValueAsString();
2479                                if ("CodeSystem.concept.code".equals(path)) {
2480                                        systemAsString = myUseSystem;
2481                                } else if ("ValueSet.codeSystem.concept.code".equals(path)) {
2482                                        systemAsString = myUseSystem;
2483                                }
2484
2485                                if (value instanceof IIdType) {
2486                                        valueAsString = ((IIdType) value).getIdPart();
2487                                }
2488
2489                                BaseSearchParamExtractor.this.createTokenIndexIfNotBlankAndAdd(
2490                                                myResourceTypeName, params, searchParam, systemAsString, valueAsString);
2491                                return;
2492                        }
2493
2494                        switch (path) {
2495                                case "Patient.communication":
2496                                        BaseSearchParamExtractor.this.addToken_PatientCommunication(
2497                                                        myResourceTypeName, params, searchParam, value);
2498                                        return;
2499                                case "Consent.source":
2500                                        // Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
2501                                        return;
2502                                case "Location.position":
2503                                        BaseSearchParamExtractor.this.addCoords_Position(myResourceTypeName, params, searchParam, value);
2504                                        return;
2505                                case "StructureDefinition.context":
2506                                        // TODO: implement this
2507                                        ourLog.warn("StructureDefinition context indexing not currently supported");
2508                                        return;
2509                                case "CapabilityStatement.rest.security":
2510                                        BaseSearchParamExtractor.this.addToken_CapabilityStatementRestSecurity(
2511                                                        myResourceTypeName, params, searchParam, value);
2512                                        return;
2513                        }
2514
2515                        String nextType = BaseSearchParamExtractor.this.toRootTypeName(value);
2516                        switch (nextType) {
2517                                case "Identifier":
2518                                        addToken_Identifier(myResourceTypeName, params, searchParam, value);
2519                                        break;
2520                                case "CodeableConcept":
2521                                        addToken_CodeableConcept(myResourceTypeName, params, searchParam, value);
2522                                        break;
2523                                case "CodeableReference":
2524                                        addToken_CodeableReference(myResourceTypeName, params, searchParam, value);
2525                                        break;
2526                                case "Coding":
2527                                        addToken_Coding(myResourceTypeName, params, searchParam, value);
2528                                        break;
2529                                case "ContactPoint":
2530                                        addToken_ContactPoint(myResourceTypeName, params, searchParam, value);
2531                                        break;
2532                                case "Range":
2533                                        // Group.characteristic.value[x] can have a Range - Ignored for now
2534                                        break;
2535                                case "Quantity":
2536                                        // Group.characteristic.value[x] can have a Quantity - Ignored for now
2537                                        break;
2538                                default:
2539                                        addUnexpectedDatatypeWarning(params, searchParam, value, path);
2540                                        break;
2541                        }
2542                }
2543        }
2544
2545        /**
2546         * Extractor that delegates to two other extractors.
2547         *
2548         * @param <T> the type (currently only used for Numeric)
2549         */
2550        private static class MultiplexExtractor<T> implements IExtractor<T> {
2551
2552                private final IExtractor<T> myExtractor0;
2553                private final IExtractor<T> myExtractor1;
2554
2555                private MultiplexExtractor(IExtractor<T> theExtractor0, IExtractor<T> theExtractor1) {
2556                        myExtractor0 = theExtractor0;
2557                        myExtractor1 = theExtractor1;
2558                }
2559
2560                @Override
2561                public void extract(
2562                                SearchParamSet<T> theParams,
2563                                RuntimeSearchParam theSearchParam,
2564                                IBase theValue,
2565                                String thePath,
2566                                boolean theWantLocalReferences) {
2567                        myExtractor0.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
2568                        myExtractor1.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
2569                }
2570        }
2571
2572        public void setExtractResourceLevelParams(boolean theExtractResourceLevelParams) {
2573                myExtractResourceLevelParams = theExtractResourceLevelParams;
2574        }
2575}