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