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