001/*-
002 * #%L
003 * HAPI FHIR - Core Library
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.util;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.FhirVersionEnum;
025import ca.uhn.fhir.context.RuntimeResourceDefinition;
026import ca.uhn.fhir.context.RuntimeSearchParam;
027import ca.uhn.fhir.i18n.Msg;
028import jakarta.annotation.Nonnull;
029import jakarta.annotation.Nullable;
030import org.apache.commons.collections4.CollectionUtils;
031import org.apache.commons.lang3.Validate;
032import org.hl7.fhir.instance.model.api.IBase;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IPrimitiveType;
035
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.List;
039import java.util.Optional;
040import java.util.Set;
041import java.util.stream.Collectors;
042
043public class SearchParameterUtil {
044
045        public static List<String> getBaseAsStrings(FhirContext theContext, IBaseResource theResource) {
046                Validate.notNull(theContext, "theContext must not be null");
047                Validate.notNull(theResource, "theResource must not be null");
048                RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
049
050                BaseRuntimeChildDefinition base = def.getChildByName("base");
051                List<IBase> baseValues = base.getAccessor().getValues(theResource);
052                List<String> retVal = new ArrayList<>();
053                for (IBase next : baseValues) {
054                        IPrimitiveType<?> nextPrimitive = (IPrimitiveType<?>) next;
055                        retVal.add(nextPrimitive.getValueAsString());
056                }
057
058                return retVal;
059        }
060
061        /**
062         * Given the resource type, fetch its patient-based search parameter name
063         * 1. Attempt to find one called 'patient'
064         * 2. If that fails, find one called 'subject'
065         * 3. If that fails, find one by Patient Compartment.
066         * 3.1 If that returns exactly 1 result then return it
067         * 3.2 If that doesn't return exactly 1 result and is R4, fall to 3.3, otherwise, 3.5
068         * 3.3 If that returns >1 result, throw an error
069         * 3.4 If that returns 1 result, return it
070         * 3.5 Find the search parameters by patient compartment using the R4 FHIR path, and return it if there is 1 result,
071         *     otherwise, fall to 3.3
072         */
073        public static Optional<RuntimeSearchParam> getOnlyPatientSearchParamForResourceType(
074                        FhirContext theFhirContext, String theResourceType) {
075                RuntimeSearchParam myPatientSearchParam = null;
076                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
077                myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
078                if (myPatientSearchParam == null) {
079                        myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject");
080                        if (myPatientSearchParam == null) {
081                                final List<RuntimeSearchParam> searchParamsForCurrentVersion =
082                                                runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
083                                final List<RuntimeSearchParam> searchParamsToUse;
084                                // We want to handle a narrow code path in which attempting to process SearchParameters for a non-R4
085                                // resource would have failed, and instead make another attempt to process them with the R4-equivalent
086                                // FHIR path.
087                                if (FhirVersionEnum.R4 == theFhirContext.getVersion().getVersion()
088                                                || searchParamsForCurrentVersion.size() == 1) {
089                                        searchParamsToUse = searchParamsForCurrentVersion;
090                                } else {
091                                        searchParamsToUse =
092                                                        checkR4PatientCompartmentForMatchingSearchParam(runtimeResourceDefinition, theResourceType);
093                                }
094                                myPatientSearchParam =
095                                                validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, searchParamsToUse);
096                        }
097                }
098                return Optional.of(myPatientSearchParam);
099        }
100
101        @Nonnull
102        private static List<RuntimeSearchParam> checkR4PatientCompartmentForMatchingSearchParam(
103                        RuntimeResourceDefinition theRuntimeResourceDefinition, String theResourceType) {
104                final RuntimeSearchParam patientSearchParamForR4 =
105                                FhirContext.forR4Cached().getResourceDefinition(theResourceType).getSearchParam("patient");
106
107                return Optional.ofNullable(patientSearchParamForR4)
108                                .map(patientSearchParamForR4NonNull ->
109                                                theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient").stream()
110                                                                .filter(searchParam -> searchParam.getPath() != null)
111                                                                .filter(searchParam ->
112                                                                                searchParam.getPath().equals(patientSearchParamForR4NonNull.getPath()))
113                                                                .collect(Collectors.toList()))
114                                .orElse(Collections.emptyList());
115        }
116
117        /**
118         * Given the resource type, fetch all its patient-based search parameter name that's available
119         */
120        public static Set<String> getPatientSearchParamsForResourceType(
121                        FhirContext theFhirContext, String theResourceType) {
122                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
123
124                List<RuntimeSearchParam> searchParams =
125                                new ArrayList<>(runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
126                // add patient search parameter for resources that's not in the compartment
127                RuntimeSearchParam myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
128                if (myPatientSearchParam != null) {
129                        searchParams.add(myPatientSearchParam);
130                }
131                RuntimeSearchParam mySubjectSearchParam = runtimeResourceDefinition.getSearchParam("subject");
132                if (mySubjectSearchParam != null) {
133                        searchParams.add(mySubjectSearchParam);
134                }
135                if (CollectionUtils.isEmpty(searchParams)) {
136                        String errorMessage = String.format(
137                                        "Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
138                                        runtimeResourceDefinition.getId());
139                        throw new IllegalArgumentException(Msg.code(2222) + errorMessage);
140                }
141                // deduplicate list of searchParams and get their names
142                return searchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.toSet());
143        }
144
145        /**
146         * Search the resource definition for a compartment named 'patient' and return its related Search Parameter.
147         */
148        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
149                        FhirContext theFhirContext, String theResourceType) {
150                RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
151                return getOnlyPatientCompartmentRuntimeSearchParam(resourceDefinition);
152        }
153
154        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
155                        RuntimeResourceDefinition runtimeResourceDefinition) {
156                return validateSearchParamsAndReturnOnlyOne(
157                                runtimeResourceDefinition, runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
158        }
159
160        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
161                        RuntimeResourceDefinition runtimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
162                return validateSearchParamsAndReturnOnlyOne(runtimeResourceDefinition, theSearchParams);
163        }
164
165        @Nonnull
166        private static RuntimeSearchParam validateSearchParamsAndReturnOnlyOne(
167                        RuntimeResourceDefinition theRuntimeResourceDefinition, List<RuntimeSearchParam> theSearchParams) {
168                final RuntimeSearchParam patientSearchParam;
169                if (CollectionUtils.isEmpty(theSearchParams)) {
170                        String errorMessage = String.format(
171                                        "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",
172                                        theRuntimeResourceDefinition.getName(),
173                                        theRuntimeResourceDefinition.getId(),
174                                        theRuntimeResourceDefinition.getStructureVersion());
175                        throw new IllegalArgumentException(Msg.code(1774) + errorMessage);
176                } else if (theSearchParams.size() == 1) {
177                        patientSearchParam = theSearchParams.get(0);
178                } else {
179                        String errorMessage = String.format(
180                                        "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.",
181                                        theRuntimeResourceDefinition.getName(),
182                                        theRuntimeResourceDefinition.getId(),
183                                        theRuntimeResourceDefinition.getStructureVersion());
184                        throw new IllegalArgumentException(Msg.code(1775) + errorMessage);
185                }
186                return patientSearchParam;
187        }
188
189        public static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParamsForResourceType(
190                        FhirContext theFhirContext, String theResourceType) {
191                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
192                return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition);
193        }
194
195        public static List<RuntimeSearchParam> getAllPatientCompartmenRuntimeSearchParams(FhirContext theFhirContext) {
196                return theFhirContext.getResourceTypes().stream()
197                                .flatMap(type ->
198                                                getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type).stream())
199                                .collect(Collectors.toList());
200        }
201
202        public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) {
203                return theFhirContext.getResourceTypes().stream()
204                                .filter(type -> CollectionUtils.isNotEmpty(
205                                                getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)))
206                                .collect(Collectors.toSet());
207        }
208
209        private static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParams(
210                        RuntimeResourceDefinition theRuntimeResourceDefinition) {
211                List<RuntimeSearchParam> patient = theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
212                return patient;
213        }
214
215        /**
216         * Return true if any search parameter in the resource can point at a patient, false otherwise
217         */
218        public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) {
219                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
220                return CollectionUtils.isNotEmpty(getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition));
221        }
222
223        @Nullable
224        public static String getCode(FhirContext theContext, IBaseResource theResource) {
225                return getStringChild(theContext, theResource, "code");
226        }
227
228        @Nullable
229        public static String getURL(FhirContext theContext, IBaseResource theResource) {
230                return getStringChild(theContext, theResource, "url");
231        }
232
233        @Nullable
234        public static String getExpression(FhirContext theFhirContext, IBaseResource theResource) {
235                return getStringChild(theFhirContext, theResource, "expression");
236        }
237
238        private static String getStringChild(FhirContext theFhirContext, IBaseResource theResource, String theChildName) {
239                Validate.notNull(theFhirContext, "theContext must not be null");
240                Validate.notNull(theResource, "theResource must not be null");
241                RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theResource);
242
243                BaseRuntimeChildDefinition base = def.getChildByName(theChildName);
244                return base.getAccessor()
245                                .getFirstValueOrNull(theResource)
246                                .map(t -> ((IPrimitiveType<?>) t))
247                                .map(t -> t.getValueAsString())
248                                .orElse(null);
249        }
250
251        public static String stripModifier(String theSearchParam) {
252                String retval;
253                int colonIndex = theSearchParam.indexOf(":");
254                if (colonIndex == -1) {
255                        retval = theSearchParam;
256                } else {
257                        retval = theSearchParam.substring(0, colonIndex);
258                }
259                return retval;
260        }
261}