001/*-
002 * #%L
003 * HAPI FHIR - Core Library
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.util;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.FhirVersionEnum;
027import ca.uhn.fhir.context.RuntimeResourceDefinition;
028import ca.uhn.fhir.context.RuntimeSearchParam;
029import ca.uhn.fhir.i18n.Msg;
030import jakarta.annotation.Nonnull;
031import jakarta.annotation.Nullable;
032import org.apache.commons.collections4.CollectionUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.lang3.Validate;
035import org.hl7.fhir.instance.model.api.IBase;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037import org.hl7.fhir.instance.model.api.IPrimitiveType;
038
039import java.util.ArrayList;
040import java.util.Collections;
041import java.util.List;
042import java.util.Optional;
043import java.util.Set;
044import java.util.stream.Collectors;
045
046import static org.apache.commons.lang3.StringUtils.isBlank;
047
048public class SearchParameterUtil {
049
050        public static List<String> getBaseAsStrings(FhirContext theContext, IBaseResource theResource) {
051                Validate.notNull(theContext, "theContext must not be null");
052                Validate.notNull(theResource, "theResource must not be null");
053                RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
054
055                BaseRuntimeChildDefinition base = def.getChildByName("base");
056                List<IBase> baseValues = base.getAccessor().getValues(theResource);
057                List<String> retVal = new ArrayList<>();
058                for (IBase next : baseValues) {
059                        IPrimitiveType<?> nextPrimitive = (IPrimitiveType<?>) next;
060                        retVal.add(nextPrimitive.getValueAsString());
061                }
062
063                return retVal;
064        }
065
066        /**
067         * Given the resource type, fetch its patient-based search parameter name
068         * 1. Attempt to find one called 'patient'
069         * 2. If that fails, find one called 'subject'
070         * 3. If that fails, find one by Patient Compartment.
071         * 3.1 If that returns exactly 1 result then return it
072         * 3.2 If that doesn't return exactly 1 result and is R4, fall to 3.3, otherwise, 3.5
073         * 3.3 If that returns >1 result, throw an error
074         * 3.4 If that returns 1 result, return it
075         * 3.5 Find the search parameters by patient compartment using the R4 FHIR path, and return it if there is 1 result,
076         * otherwise, fall to 3.3
077         */
078        public static Optional<RuntimeSearchParam> getOnlyPatientSearchParamForResourceType(
079                        FhirContext theFhirContext, String theResourceType) {
080                RuntimeSearchParam myPatientSearchParam = null;
081                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
082                myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
083                if (myPatientSearchParam == null) {
084                        myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject");
085                        if (myPatientSearchParam == null) {
086                                final List<RuntimeSearchParam> searchParamsForCurrentVersion =
087                                                runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
088                                final List<RuntimeSearchParam> searchParamsToUse;
089                                // We want to handle a narrow code path in which attempting to process SearchParameters for a non-R4
090                                // resource would have failed, and instead make another attempt to process them with the R4-equivalent
091                                // FHIR path.
092                                if (FhirVersionEnum.R4 == theFhirContext.getVersion().getVersion()
093                                                || searchParamsForCurrentVersion.size() == 1) {
094                                        searchParamsToUse = searchParamsForCurrentVersion;
095                                } else {
096                                        searchParamsToUse =
097                                                        checkR4PatientCompartmentForMatchingSearchParam(runtimeResourceDefinition, theResourceType);
098                                }
099                                myPatientSearchParam =
100                                                validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, searchParamsToUse);
101                        }
102                }
103                return Optional.of(myPatientSearchParam);
104        }
105
106        @Nonnull
107        private static List<RuntimeSearchParam> checkR4PatientCompartmentForMatchingSearchParam(
108                        RuntimeResourceDefinition theRuntimeResourceDefinition, String theResourceType) {
109                final RuntimeSearchParam patientSearchParamForR4 =
110                                FhirContext.forR4Cached().getResourceDefinition(theResourceType).getSearchParam("patient");
111
112                return Optional.ofNullable(patientSearchParamForR4)
113                                .map(patientSearchParamForR4NonNull ->
114                                                theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient").stream()
115                                                                .filter(searchParam -> searchParam.getPath() != null)
116                                                                .filter(searchParam ->
117                                                                                searchParam.getPath().equals(patientSearchParamForR4NonNull.getPath()))
118                                                                .collect(Collectors.toList()))
119                                .orElse(Collections.emptyList());
120        }
121
122        /**
123         * Given the resource type, fetch all its patient-based search parameter name that's available
124         */
125        public static Set<String> getPatientSearchParamsForResourceType(
126                        FhirContext theFhirContext, String theResourceType) {
127                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
128
129                List<RuntimeSearchParam> searchParams =
130                                new ArrayList<>(runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
131                // add patient search parameter for resources that's not in the compartment
132                RuntimeSearchParam myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
133                if (myPatientSearchParam != null) {
134                        searchParams.add(myPatientSearchParam);
135                }
136                RuntimeSearchParam mySubjectSearchParam = runtimeResourceDefinition.getSearchParam("subject");
137                if (mySubjectSearchParam != null) {
138                        searchParams.add(mySubjectSearchParam);
139                }
140                if (CollectionUtils.isEmpty(searchParams)) {
141                        String errorMessage = String.format(
142                                        "Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
143                                        runtimeResourceDefinition.getId());
144                        throw new IllegalArgumentException(Msg.code(2222) + errorMessage);
145                }
146                // deduplicate list of searchParams and get their names
147                return searchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.toSet());
148        }
149
150        /**
151         * Search the resource definition for a compartment named 'patient' and return its related Search Parameter.
152         */
153        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
154                        FhirContext theFhirContext, String theResourceType) {
155                RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
156                return getOnlyPatientCompartmentRuntimeSearchParam(resourceDefinition);
157        }
158
159        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
160                        RuntimeResourceDefinition runtimeResourceDefinition) {
161                return validateSearchParamsAndReturnOnlyOne(
162                                runtimeResourceDefinition, runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
163        }
164
165        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
166                        RuntimeResourceDefinition runtimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
167                return validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, theSearchParams);
168        }
169
170        @Nonnull
171        private static RuntimeSearchParam validateSearchParamsAndReturnOnlyOne(
172                        RuntimeResourceDefinition theRuntimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
173                final RuntimeSearchParam patientSearchParam;
174                if (CollectionUtils.isEmpty(theSearchParams)) {
175                        String errorMessage = String.format(
176                                        "Resource type [%s] for ID [%s] and version: [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
177                                        theRuntimeResourceDefinition.getName(),
178                                        theRuntimeResourceDefinition.getId(),
179                                        theRuntimeResourceDefinition.getStructureVersion());
180                        throw new IllegalArgumentException(Msg.code(1774) + errorMessage);
181                } else if (theSearchParams.size() == 1) {
182                        patientSearchParam = theSearchParams.get(0);
183                } else {
184                        String errorMessage = String.format(
185                                        "Resource type [%s] for ID [%s] and version: [%s] has more than one Search Param which references a patient compartment. We are unable to disambiguate which patient search parameter we should be searching by.",
186                                        theRuntimeResourceDefinition.getName(),
187                                        theRuntimeResourceDefinition.getId(),
188                                        theRuntimeResourceDefinition.getStructureVersion());
189                        throw new IllegalArgumentException(Msg.code(1775) + errorMessage);
190                }
191                return patientSearchParam;
192        }
193
194        public static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParamsForResourceType(
195                        FhirContext theFhirContext, String theResourceType) {
196                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
197                return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition);
198        }
199
200        public static List<RuntimeSearchParam> getAllPatientCompartmenRuntimeSearchParams(FhirContext theFhirContext) {
201                return theFhirContext.getResourceTypes().stream()
202                                .flatMap(type ->
203                                                getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type).stream())
204                                .collect(Collectors.toList());
205        }
206
207        public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) {
208                return theFhirContext.getResourceTypes().stream()
209                                .filter(type -> CollectionUtils.isNotEmpty(
210                                                getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)))
211                                .collect(Collectors.toSet());
212        }
213
214        private static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParams(
215                        RuntimeResourceDefinition theRuntimeResourceDefinition) {
216                List<RuntimeSearchParam> patient = theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
217                return patient;
218        }
219
220        /**
221         * Return true if any search parameter in the resource can point at a patient, false otherwise
222         */
223        public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) {
224                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
225                return CollectionUtils.isNotEmpty(getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition));
226        }
227
228        @Nullable
229        public static String getCode(FhirContext theContext, IBaseResource theResource) {
230                return getStringChild(theContext, theResource, "code");
231        }
232
233        @Nullable
234        public static String getURL(FhirContext theContext, IBaseResource theResource) {
235                return getStringChild(theContext, theResource, "url");
236        }
237
238        @Nullable
239        public static String getExpression(FhirContext theFhirContext, IBaseResource theResource) {
240                return getStringChild(theFhirContext, theResource, "expression");
241        }
242
243        private static String getStringChild(FhirContext theFhirContext, IBaseResource theResource, String theChildName) {
244                Validate.notNull(theFhirContext, "theContext must not be null");
245                Validate.notNull(theResource, "theResource must not be null");
246                RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theResource);
247
248                BaseRuntimeChildDefinition base = def.getChildByName(theChildName);
249                return base.getAccessor()
250                                .getFirstValueOrNull(theResource)
251                                .map(t -> ((IPrimitiveType<?>) t))
252                                .map(t -> t.getValueAsString())
253                                .orElse(null);
254        }
255
256        public static String stripModifier(String theSearchParam) {
257                String retval;
258                int colonIndex = theSearchParam.indexOf(":");
259                if (colonIndex == -1) {
260                        retval = theSearchParam;
261                } else {
262                        retval = theSearchParam.substring(0, colonIndex);
263                }
264                return retval;
265        }
266
267        /**
268         * Many SearchParameters combine a series of potential expressions into a single concatenated
269         * expression. For example, in FHIR R5 the "encounter" search parameter has an expression like:
270         * <code>AuditEvent.encounter | CarePlan.encounter | ChargeItem.encounter | ......</code>.
271         * This method takes such a FHIRPath expression and splits it into a series of separate
272         * expressions. To achieve this, we iteratively splits a string on any <code> or </code> or <code>|</code> that
273         * is <b>not</b> contained inside a set of parentheses. e.g.
274         * <p>
275         * "Patient.select(a or b)" -->  ["Patient.select(a or b)"]
276         * "Patient.select(a or b) or Patient.select(c or d )" --> ["Patient.select(a or b)", "Patient.select(c or d)"]
277         * "Patient.select(a|b) or Patient.select(c or d )" --> ["Patient.select(a|b)", "Patient.select(c or d)"]
278         * "Patient.select(b) | Patient.select(c)" -->  ["Patient.select(b)", "Patient.select(c)"]
279         *
280         * @param thePaths The FHIRPath expression to split
281         * @return The split strings
282         */
283        public static String[] splitSearchParameterExpressions(String thePaths) {
284                if (!StringUtils.containsAny(thePaths, " or ", " |")) {
285                        return new String[] {thePaths};
286                }
287                List<String> topLevelOrExpressions = splitOutOfParensToken(thePaths, " or ");
288                return topLevelOrExpressions.stream()
289                                .flatMap(s -> splitOutOfParensToken(s, " |").stream())
290                                .toArray(String[]::new);
291        }
292
293        private static List<String> splitOutOfParensToken(String thePath, String theToken) {
294                int tokenLength = theToken.length();
295                int index = thePath.indexOf(theToken);
296                int rightIndex = 0;
297                List<String> retVal = new ArrayList<>();
298                while (index > -1) {
299                        String left = thePath.substring(rightIndex, index);
300                        if (allParensHaveBeenClosed(left)) {
301                                retVal.add(left.trim());
302                                rightIndex = index + tokenLength;
303                        }
304                        index = thePath.indexOf(theToken, index + tokenLength);
305                }
306                String pathTrimmed = thePath.substring(rightIndex).trim();
307                if (!pathTrimmed.isEmpty()) {
308                        retVal.add(pathTrimmed);
309                }
310                return retVal;
311        }
312
313        private static boolean allParensHaveBeenClosed(String thePaths) {
314                int open = StringUtils.countMatches(thePaths, "(");
315                int close = StringUtils.countMatches(thePaths, ")");
316                return open == close;
317        }
318
319        /**
320         * Given a FHIRPath expression which presumably addresses a FHIR reference or
321         * canonical reference element (i.e. a FHIRPath expression used in a "reference"
322         * SearchParameter), tries to determine whether the path could potentially resolve
323         * to a canonical reference.
324         * <p>
325         * Just because a SearchParameter is a Reference SP, doesn't necessarily mean that it
326         * can reference a canonical. So first we try to rule out the SP based on the path it
327         * contains. This matters because a SearchParameter of type Reference can point to
328         * a canonical element (in which case we need to _include any canonical targets). Or it
329         * can point to a Reference element (in which case we only need to _include actual
330         * references by ID).
331         * </p>
332         * <p>
333         * This isn't perfect because there's really no definitive and comprehensive
334         * way of determining the datatype that a SearchParameter or a FHIRPath point to. But
335         * we do our best if the path is simple enough to just manually check the type it
336         * points to, or if it ends in an explicit type declaration.
337         * </p>
338         * <p>
339         * Because it is not possible to deterministically determine the datatype for a
340         * FHIRPath expression in all cases, this method is cautious: it will return
341         * {@literal true} if it isn't sure.
342         * </p>
343         *
344         * @return Returns {@literal true} if the path could return a {@literal canonical} or {@literal CanonicalReference}, or returns {@literal false} if the path could only return a {@literal Reference}.
345         */
346        public static boolean referencePathCouldPotentiallyReferenceCanonicalElement(
347                        FhirContext theContext, String theResourceType, String thePath, boolean theReverse) {
348
349                // If this path explicitly wants a reference and not a canonical, we can ignore it since we're
350                // only looking for canonicals here
351                if (thePath.endsWith(".ofType(Reference)")) {
352                        return false;
353                }
354
355                BaseRuntimeElementCompositeDefinition<?> currentDef = theContext.getResourceDefinition(theResourceType);
356
357                String remainingPath = thePath;
358                boolean firstSegment = true;
359                while (remainingPath != null) {
360
361                        int dotIdx = remainingPath.indexOf(".");
362                        String currentSegment;
363                        if (dotIdx == -1) {
364                                currentSegment = remainingPath;
365                                remainingPath = null;
366                        } else {
367                                currentSegment = remainingPath.substring(0, dotIdx);
368                                remainingPath = remainingPath.substring(dotIdx + 1);
369                        }
370
371                        if (isBlank(currentSegment)) {
372                                return true;
373                        }
374
375                        if (firstSegment) {
376                                firstSegment = false;
377                                if (Character.isUpperCase(currentSegment.charAt(0))) {
378                                        // This is just the resource name
379                                        if (!theReverse && !theResourceType.equals(currentSegment)) {
380                                                return false;
381                                        }
382                                        continue;
383                                }
384                        }
385
386                        BaseRuntimeChildDefinition child = currentDef.getChildByName(currentSegment);
387                        if (child == null) {
388                                return true;
389                        }
390                        BaseRuntimeElementDefinition<?> def = child.getChildByName(currentSegment);
391                        if (def == null) {
392                                return true;
393                        }
394                        if (def.getName().equals("Reference")) {
395                                return false;
396                        }
397                        if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
398                                return true;
399                        }
400
401                        currentDef = (BaseRuntimeElementCompositeDefinition<?>) def;
402                }
403
404                return true;
405        }
406}