001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.searchparam.registry;
021
022import ca.uhn.fhir.context.ComboSearchParamType;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.model.api.ExtensionDt;
028import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
030import ca.uhn.fhir.util.DatatypeUtil;
031import ca.uhn.fhir.util.ExtensionUtil;
032import ca.uhn.fhir.util.FhirTerser;
033import ca.uhn.fhir.util.HapiExtensions;
034import ca.uhn.fhir.util.PhoneticEncoderUtil;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.commons.lang3.Strings;
037import org.hl7.fhir.dstu3.model.Extension;
038import org.hl7.fhir.dstu3.model.SearchParameter;
039import org.hl7.fhir.instance.model.api.IBase;
040import org.hl7.fhir.instance.model.api.IBaseDatatype;
041import org.hl7.fhir.instance.model.api.IBaseExtension;
042import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
043import org.hl7.fhir.instance.model.api.IBaseResource;
044import org.hl7.fhir.instance.model.api.IIdType;
045import org.hl7.fhir.instance.model.api.IPrimitiveType;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048import org.springframework.beans.factory.annotation.Autowired;
049import org.springframework.stereotype.Service;
050
051import java.util.ArrayList;
052import java.util.Collection;
053import java.util.Collections;
054import java.util.HashSet;
055import java.util.List;
056import java.util.Set;
057import java.util.stream.Collectors;
058
059import static org.apache.commons.lang3.StringUtils.isBlank;
060import static org.apache.commons.lang3.StringUtils.isNotBlank;
061
062@Service
063public class SearchParameterCanonicalizer {
064        private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizer.class);
065
066        private final FhirContext myFhirContext;
067        private final FhirTerser myTerser;
068
069        @Autowired
070        public SearchParameterCanonicalizer(FhirContext theFhirContext) {
071                myFhirContext = theFhirContext;
072                myTerser = myFhirContext.newTerser();
073        }
074
075        private static Collection<String> toStrings(Collection<? extends IPrimitiveType<String>> theBase) {
076                HashSet<String> retVal = new HashSet<>();
077                for (IPrimitiveType<String> next : theBase) {
078                        if (isNotBlank(next.getValueAsString())) {
079                                retVal.add(next.getValueAsString());
080                        }
081                }
082                return retVal;
083        }
084
085        public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchParameter) {
086                RuntimeSearchParam retVal;
087                switch (myFhirContext.getVersion().getVersion()) {
088                        case DSTU2:
089                                retVal = canonicalizeSearchParameterDstu2(
090                                                (ca.uhn.fhir.model.dstu2.resource.SearchParameter) theSearchParameter);
091                                break;
092                        case DSTU3:
093                                retVal =
094                                                canonicalizeSearchParameterDstu3((org.hl7.fhir.dstu3.model.SearchParameter) theSearchParameter);
095                                break;
096                        case R4:
097                        case R4B:
098                        case R5:
099                                retVal = canonicalizeSearchParameterR4Plus(theSearchParameter);
100                                break;
101                        case DSTU2_HL7ORG:
102                        case DSTU2_1:
103                                // Non-supported - these won't happen so just fall through
104                        default:
105                                throw new InternalErrorException(
106                                                Msg.code(510) + "SearchParameter canonicalization not supported for FHIR version"
107                                                                + myFhirContext.getVersion().getVersion());
108                }
109
110                if (retVal != null) {
111                        extractExtensions(theSearchParameter, retVal);
112                }
113
114                return retVal;
115        }
116
117        private RuntimeSearchParam canonicalizeSearchParameterDstu2(
118                        ca.uhn.fhir.model.dstu2.resource.SearchParameter theNextSp) {
119                String name = theNextSp.getCode();
120                String description = theNextSp.getDescription();
121                String path = theNextSp.getXpath();
122
123                Collection<String> baseResource = toStrings(Collections.singletonList(theNextSp.getBaseElement()));
124                List<String> baseCustomResources = extractDstu2CustomResourcesFromExtensions(
125                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
126
127                if (!baseCustomResources.isEmpty()) {
128                        baseResource = Collections.singleton(baseCustomResources.get(0));
129                }
130
131                RestSearchParameterTypeEnum paramType = null;
132                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
133                if (theNextSp.getTypeElement().getValueAsEnum() != null) {
134                        switch (theNextSp.getTypeElement().getValueAsEnum()) {
135                                case COMPOSITE:
136                                        paramType = RestSearchParameterTypeEnum.COMPOSITE;
137                                        break;
138                                case DATE_DATETIME:
139                                        paramType = RestSearchParameterTypeEnum.DATE;
140                                        break;
141                                case NUMBER:
142                                        paramType = RestSearchParameterTypeEnum.NUMBER;
143                                        break;
144                                case QUANTITY:
145                                        paramType = RestSearchParameterTypeEnum.QUANTITY;
146                                        break;
147                                case REFERENCE:
148                                        paramType = RestSearchParameterTypeEnum.REFERENCE;
149                                        break;
150                                case STRING:
151                                        paramType = RestSearchParameterTypeEnum.STRING;
152                                        break;
153                                case TOKEN:
154                                        paramType = RestSearchParameterTypeEnum.TOKEN;
155                                        break;
156                                case URI:
157                                        paramType = RestSearchParameterTypeEnum.URI;
158                                        break;
159                        }
160                }
161                if (theNextSp.getStatus() != null) {
162                        switch (theNextSp.getStatusElement().getValueAsEnum()) {
163                                case ACTIVE:
164                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
165                                        break;
166                                case DRAFT:
167                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
168                                        break;
169                                case RETIRED:
170                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
171                                        break;
172                        }
173                }
174
175                Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
176                List<String> targetCustomResources = extractDstu2CustomResourcesFromExtensions(
177                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
178
179                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
180
181                if (isBlank(name) || isBlank(path)) {
182                        if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
183                                return null;
184                        }
185                }
186
187                IIdType id = theNextSp.getIdElement();
188                String uri = "";
189                ComboSearchParamType unique = null;
190
191                List<ExtensionDt> uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE);
192                if (uniqueExts.size() > 0) {
193                        IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive();
194                        if (uniqueExtsValuePrimitive != null) {
195                                if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
196                                        unique = ComboSearchParamType.UNIQUE;
197                                } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
198                                        unique = ComboSearchParamType.NON_UNIQUE;
199                                }
200                        }
201                }
202
203                List<RuntimeSearchParam.Component> components = Collections.emptyList();
204                return new RuntimeSearchParam(
205                                id,
206                                uri,
207                                name,
208                                description,
209                                path,
210                                paramType,
211                                Collections.emptySet(),
212                                targetResources,
213                                status,
214                                unique,
215                                components,
216                                baseResource);
217        }
218
219        private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) {
220                String name = theNextSp.getCode();
221                String description = theNextSp.getDescription();
222                String path = theNextSp.getExpression();
223
224                List<String> baseResources = new ArrayList<>(toStrings(theNextSp.getBase()));
225                List<String> baseCustomResources = extractDstu3CustomResourcesFromExtensions(
226                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
227
228                maybeAddCustomResourcesToResources(baseResources, baseCustomResources);
229
230                RestSearchParameterTypeEnum paramType = null;
231                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
232                if (theNextSp.getType() != null) {
233                        switch (theNextSp.getType()) {
234                                case COMPOSITE:
235                                        paramType = RestSearchParameterTypeEnum.COMPOSITE;
236                                        break;
237                                case DATE:
238                                        paramType = RestSearchParameterTypeEnum.DATE;
239                                        break;
240                                case NUMBER:
241                                        paramType = RestSearchParameterTypeEnum.NUMBER;
242                                        break;
243                                case QUANTITY:
244                                        paramType = RestSearchParameterTypeEnum.QUANTITY;
245                                        break;
246                                case REFERENCE:
247                                        paramType = RestSearchParameterTypeEnum.REFERENCE;
248                                        break;
249                                case STRING:
250                                        paramType = RestSearchParameterTypeEnum.STRING;
251                                        break;
252                                case TOKEN:
253                                        paramType = RestSearchParameterTypeEnum.TOKEN;
254                                        break;
255                                case URI:
256                                        paramType = RestSearchParameterTypeEnum.URI;
257                                        break;
258                                case NULL:
259                                        break;
260                        }
261                }
262                if (theNextSp.getStatus() != null) {
263                        switch (theNextSp.getStatus()) {
264                                case ACTIVE:
265                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
266                                        break;
267                                case DRAFT:
268                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
269                                        break;
270                                case RETIRED:
271                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
272                                        break;
273                                case UNKNOWN:
274                                        status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN;
275                                        break;
276                                case NULL:
277                                        break;
278                        }
279                }
280
281                Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget());
282                List<String> targetCustomResources = extractDstu3CustomResourcesFromExtensions(
283                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
284
285                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
286
287                if (isBlank(name) || isBlank(path) || paramType == null) {
288                        if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
289                                return null;
290                        }
291                }
292
293                IIdType id = theNextSp.getIdElement();
294                String uri = "";
295                ComboSearchParamType unique = null;
296
297                List<Extension> uniqueExts = theNextSp.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE);
298                if (uniqueExts.size() > 0) {
299                        IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive();
300                        if (uniqueExtsValuePrimitive != null) {
301                                if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
302                                        unique = ComboSearchParamType.UNIQUE;
303                                } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) {
304                                        unique = ComboSearchParamType.NON_UNIQUE;
305                                }
306                        }
307                }
308
309                List<RuntimeSearchParam.Component> components = new ArrayList<>();
310                for (SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) {
311                        components.add(new RuntimeSearchParam.Component(
312                                        next.getExpression(),
313                                        next.getDefinition()
314                                                        .getReferenceElement()
315                                                        .toUnqualifiedVersionless()
316                                                        .getValue(),
317                                        null));
318                }
319
320                return new RuntimeSearchParam(
321                                id,
322                                uri,
323                                name,
324                                description,
325                                path,
326                                paramType,
327                                Collections.emptySet(),
328                                targetResources,
329                                status,
330                                unique,
331                                components,
332                                baseResources);
333        }
334
335        private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) {
336
337                String name = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "code");
338                String description = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "description");
339                String path = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "expression");
340
341                Set<String> baseResources = extractR4PlusResources("base", theNextSp);
342                List<String> baseCustomResources = extractR4PlusCustomResourcesFromExtensions(
343                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE);
344
345                maybeAddCustomResourcesToResources(baseResources, baseCustomResources);
346
347                RestSearchParameterTypeEnum paramType = null;
348                RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
349                switch (myTerser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) {
350                        case "composite":
351                                paramType = RestSearchParameterTypeEnum.COMPOSITE;
352                                break;
353                        case "date":
354                                paramType = RestSearchParameterTypeEnum.DATE;
355                                break;
356                        case "number":
357                                paramType = RestSearchParameterTypeEnum.NUMBER;
358                                break;
359                        case "quantity":
360                                paramType = RestSearchParameterTypeEnum.QUANTITY;
361                                break;
362                        case "reference":
363                                paramType = RestSearchParameterTypeEnum.REFERENCE;
364                                break;
365                        case "string":
366                                paramType = RestSearchParameterTypeEnum.STRING;
367                                break;
368                        case "token":
369                                paramType = RestSearchParameterTypeEnum.TOKEN;
370                                break;
371                        case "uri":
372                                paramType = RestSearchParameterTypeEnum.URI;
373                                break;
374                        case "special":
375                                paramType = RestSearchParameterTypeEnum.SPECIAL;
376                                break;
377                }
378                switch (myTerser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) {
379                        case "active":
380                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
381                                break;
382                        case "draft":
383                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT;
384                                break;
385                        case "retired":
386                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED;
387                                break;
388                        case "unknown":
389                                status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN;
390                                break;
391                }
392
393                Set<String> targetResources = extractR4PlusResources("target", theNextSp);
394                List<String> targetCustomResources = extractR4PlusCustomResourcesFromExtensions(
395                                theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE);
396
397                maybeAddCustomResourcesToResources(targetResources, targetCustomResources);
398
399                if (isBlank(name) || isBlank(path) || paramType == null) {
400                        if ("_text".equals(name) || "_content".equals(name)) {
401                                // ok
402                        } else if (paramType != RestSearchParameterTypeEnum.COMPOSITE) {
403                                return null;
404                        }
405                }
406
407                IIdType id = theNextSp.getIdElement();
408                String uri = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "url");
409                ComboSearchParamType unique = null;
410
411                String value = ((IBaseHasExtensions) theNextSp)
412                                .getExtension().stream()
413                                                .filter(e -> HapiExtensions.EXT_SP_UNIQUE.equals(e.getUrl()))
414                                                .filter(t -> t.getValue() instanceof IPrimitiveType)
415                                                .map(t -> (IPrimitiveType<?>) t.getValue())
416                                                .map(IPrimitiveType::getValueAsString)
417                                                .findFirst()
418                                                .orElse("");
419                if ("true".equalsIgnoreCase(value)) {
420                        unique = ComboSearchParamType.UNIQUE;
421                } else if ("false".equalsIgnoreCase(value)) {
422                        unique = ComboSearchParamType.NON_UNIQUE;
423                }
424
425                List<RuntimeSearchParam.Component> components = new ArrayList<>();
426                for (IBase next : myTerser.getValues(theNextSp, "component")) {
427                        String expression = myTerser.getSinglePrimitiveValueOrNull(next, "expression");
428                        String definition = myTerser.getSinglePrimitiveValueOrNull(next, "definition");
429                        if (Strings.CS.startsWith(definition, "/SearchParameter/")) {
430                                definition = definition.substring(1);
431                        }
432
433                        String comboUpliftChain = null;
434                        List<? extends IBaseExtension<?, ?>> componentExtensions = ((IBaseHasExtensions) next).getExtension();
435                        for (IBaseExtension<?, ?> nextComponentExtension : componentExtensions) {
436                                if (HapiExtensions.EXT_SP_COMBO_UPLIFT_CHAIN.equals(nextComponentExtension.getUrl())
437                                                && unique == ComboSearchParamType.NON_UNIQUE) {
438                                        IPrimitiveType<String> upliftChainValue =
439                                                        (IPrimitiveType<String>) nextComponentExtension.getValue();
440                                        comboUpliftChain = upliftChainValue.getValueAsString();
441                                }
442                        }
443
444                        components.add(new RuntimeSearchParam.Component(expression, definition, comboUpliftChain));
445                }
446
447                return new RuntimeSearchParam(
448                                id,
449                                uri,
450                                name,
451                                description,
452                                path,
453                                paramType,
454                                Collections.emptySet(),
455                                targetResources,
456                                status,
457                                unique,
458                                components,
459                                baseResources);
460        }
461
462        private Set<String> extractR4PlusResources(String thePath, IBaseResource theNextSp) {
463                return myTerser.getValues(theNextSp, thePath, IPrimitiveType.class).stream()
464                                .map(IPrimitiveType::getValueAsString)
465                                .collect(Collectors.toSet());
466        }
467
468        /**
469         * Extracts any extensions from the resource and populates an extension field in the
470         */
471        protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) {
472                if (theSearchParamResource instanceof IBaseHasExtensions) {
473                        List<? extends IBaseExtension<? extends IBaseExtension, ?>> extensions =
474                                        (List<? extends IBaseExtension<? extends IBaseExtension, ?>>)
475                                                        ((IBaseHasExtensions) theSearchParamResource).getExtension();
476                        for (IBaseExtension<? extends IBaseExtension, ?> next : extensions) {
477                                String nextUrl = next.getUrl();
478                                if (isNotBlank(nextUrl)) {
479                                        theRuntimeSearchParam.addExtension(nextUrl, next);
480                                        if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) {
481                                                setEncoder(theRuntimeSearchParam, next.getValue());
482                                        } else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) {
483                                                addUpliftRefchain(theRuntimeSearchParam, next);
484                                        } else if (HapiExtensions.EXT_SEARCHPARAM_ENABLED_FOR_SEARCHING.equals(nextUrl)) {
485                                                addEnabledForSearching(theRuntimeSearchParam, next.getValue());
486                                        }
487                                }
488                        }
489                }
490        }
491
492        private void addEnabledForSearching(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) {
493                if (theValue instanceof IPrimitiveType) {
494                        String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString();
495                        boolean enabledForSearching = Boolean.parseBoolean(stringValue);
496                        theRuntimeSearchParam.setEnabledForSearching(enabledForSearching);
497                }
498        }
499
500        @SuppressWarnings("unchecked")
501        private void addUpliftRefchain(
502                        RuntimeSearchParam theRuntimeSearchParam, IBaseExtension<? extends IBaseExtension, ?> theExtension) {
503                String code = ExtensionUtil.extractChildPrimitiveExtensionValue(
504                                theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE);
505                String elementName = ExtensionUtil.extractChildPrimitiveExtensionValue(
506                                theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME);
507                if (isNotBlank(code)) {
508                        theRuntimeSearchParam.addUpliftRefchain(code, elementName);
509                }
510        }
511
512        private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) {
513                if (theValue instanceof IPrimitiveType) {
514                        String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString();
515
516                        // every string creates a completely new encoder wrapper.
517                        // this is fine, because the runtime search parameters are constructed at startup
518                        // for every saved value
519                        IPhoneticEncoder encoder = PhoneticEncoderUtil.getEncoder(stringValue);
520                        if (encoder != null) {
521                                theRuntimeSearchParam.setPhoneticEncoder(encoder);
522                        } else {
523                                ourLog.error("Invalid PhoneticEncoderEnum value '" + stringValue + "'");
524                        }
525                }
526        }
527
528        private List<String> extractDstu2CustomResourcesFromExtensions(
529                        ca.uhn.fhir.model.dstu2.resource.SearchParameter theSearchParameter, String theExtensionUrl) {
530
531                List<ExtensionDt> customSpExtensionDt = theSearchParameter.getUndeclaredExtensionsByUrl(theExtensionUrl);
532
533                return customSpExtensionDt.stream()
534                                .map(theExtensionDt -> theExtensionDt.getValueAsPrimitive().getValueAsString())
535                                .filter(StringUtils::isNotBlank)
536                                .collect(Collectors.toList());
537        }
538
539        private List<String> extractDstu3CustomResourcesFromExtensions(
540                        org.hl7.fhir.dstu3.model.SearchParameter theSearchParameter, String theExtensionUrl) {
541
542                List<Extension> customSpExtensions = theSearchParameter.getExtensionsByUrl(theExtensionUrl);
543
544                return customSpExtensions.stream()
545                                .map(theExtension -> theExtension.getValueAsPrimitive().getValueAsString())
546                                .filter(StringUtils::isNotBlank)
547                                .collect(Collectors.toList());
548        }
549
550        private List<String> extractR4PlusCustomResourcesFromExtensions(
551                        IBaseResource theSearchParameter, String theExtensionUrl) {
552
553                List<String> retVal = new ArrayList<>();
554
555                if (theSearchParameter instanceof IBaseHasExtensions) {
556                        ((IBaseHasExtensions) theSearchParameter)
557                                        .getExtension().stream()
558                                                        .filter(t -> theExtensionUrl.equals(t.getUrl()))
559                                                        .filter(t -> t.getValue() instanceof IPrimitiveType)
560                                                        .map(t -> ((IPrimitiveType<?>) t.getValue()))
561                                                        .map(IPrimitiveType::getValueAsString)
562                                                        .filter(StringUtils::isNotBlank)
563                                                        .forEach(retVal::add);
564                }
565
566                return retVal;
567        }
568
569        private <T extends Collection<String>> void maybeAddCustomResourcesToResources(
570                        T theResources, List<String> theCustomResources) {
571                // SearchParameter base and target components require strict binding to ResourceType for dstu[2|3], R4, R4B
572                // and to Version Independent Resource Types for R5.
573                //
574                // To handle custom resources, we set a placeholder of type 'Resource' in the base or target component and
575                // define
576                // the custom resource by adding a corresponding extension with url
577                // HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE
578                // or HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE with the name of the custom resource.
579                //
580                // To provide a base/target list that contains both the resources and customResources, we need to remove the
581                // placeholders
582                // from the theResources and add theCustomResources.
583
584                if (!theCustomResources.isEmpty()) {
585                        theResources.removeAll(Collections.singleton("Resource"));
586                        theResources.addAll(theCustomResources);
587                }
588        }
589}