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