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.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.i18n.Msg;
027import jakarta.annotation.Nullable;
028import org.apache.commons.lang3.Validate;
029import org.hl7.fhir.instance.model.api.IBase;
030import org.hl7.fhir.instance.model.api.IBaseResource;
031import org.hl7.fhir.instance.model.api.IPrimitiveType;
032
033import java.util.ArrayList;
034import java.util.List;
035import java.util.Optional;
036import java.util.Set;
037import java.util.stream.Collectors;
038
039public class SearchParameterUtil {
040
041        public static List<String> getBaseAsStrings(FhirContext theContext, IBaseResource theResource) {
042                Validate.notNull(theContext, "theContext must not be null");
043                Validate.notNull(theResource, "theResource must not be null");
044                RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
045
046                BaseRuntimeChildDefinition base = def.getChildByName("base");
047                List<IBase> baseValues = base.getAccessor().getValues(theResource);
048                List<String> retVal = new ArrayList<>();
049                for (IBase next : baseValues) {
050                        IPrimitiveType<?> nextPrimitive = (IPrimitiveType<?>) next;
051                        retVal.add(nextPrimitive.getValueAsString());
052                }
053
054                return retVal;
055        }
056
057        /**
058         * Given the resource type, fetch its patient-based search parameter name
059         * 1. Attempt to find one called 'patient'
060         * 2. If that fails, find one called 'subject'
061         * 3. If that fails, find one by Patient Compartment.
062         * 3.1 If that returns >1 result, throw an error
063         * 3.2 If that returns 1 result, return it
064         */
065        public static Optional<RuntimeSearchParam> getOnlyPatientSearchParamForResourceType(
066                        FhirContext theFhirContext, String theResourceType) {
067                RuntimeSearchParam myPatientSearchParam = null;
068                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
069                myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
070                if (myPatientSearchParam == null) {
071                        myPatientSearchParam = runtimeResourceDefinition.getSearchParam("subject");
072                        if (myPatientSearchParam == null) {
073                                myPatientSearchParam = getOnlyPatientCompartmentRuntimeSearchParam(runtimeResourceDefinition);
074                        }
075                }
076                return Optional.ofNullable(myPatientSearchParam);
077        }
078
079        /**
080         * Given the resource type, fetch all its patient-based search parameter name that's available
081         */
082        public static Set<String> getPatientSearchParamsForResourceType(
083                        FhirContext theFhirContext, String theResourceType) {
084                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
085
086                List<RuntimeSearchParam> searchParams =
087                                new ArrayList<>(runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"));
088                // add patient search parameter for resources that's not in the compartment
089                RuntimeSearchParam myPatientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
090                if (myPatientSearchParam != null) {
091                        searchParams.add(myPatientSearchParam);
092                }
093                RuntimeSearchParam mySubjectSearchParam = runtimeResourceDefinition.getSearchParam("subject");
094                if (mySubjectSearchParam != null) {
095                        searchParams.add(mySubjectSearchParam);
096                }
097                if (searchParams == null || searchParams.size() == 0) {
098                        String errorMessage = String.format(
099                                        "Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
100                                        runtimeResourceDefinition.getId());
101                        throw new IllegalArgumentException(Msg.code(2222) + errorMessage);
102                }
103                // deduplicate list of searchParams and get their names
104                return searchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.toSet());
105        }
106
107        /**
108         * Search the resource definition for a compartment named 'patient' and return its related Search Parameter.
109         */
110        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
111                        FhirContext theFhirContext, String theResourceType) {
112                RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
113                return getOnlyPatientCompartmentRuntimeSearchParam(resourceDefinition);
114        }
115
116        public static RuntimeSearchParam getOnlyPatientCompartmentRuntimeSearchParam(
117                        RuntimeResourceDefinition runtimeResourceDefinition) {
118                RuntimeSearchParam patientSearchParam;
119                List<RuntimeSearchParam> searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
120                if (searchParams == null || searchParams.size() == 0) {
121                        String errorMessage = String.format(
122                                        "Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter",
123                                        runtimeResourceDefinition.getId());
124                        throw new IllegalArgumentException(Msg.code(1774) + errorMessage);
125                } else if (searchParams.size() == 1) {
126                        patientSearchParam = searchParams.get(0);
127                } else {
128                        String errorMessage = String.format(
129                                        "Resource type %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.",
130                                        runtimeResourceDefinition.getId());
131                        throw new IllegalArgumentException(Msg.code(1775) + errorMessage);
132                }
133                return patientSearchParam;
134        }
135
136        public static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParamsForResourceType(
137                        FhirContext theFhirContext, String theResourceType) {
138                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
139                return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition);
140        }
141
142        public static List<RuntimeSearchParam> getAllPatientCompartmenRuntimeSearchParams(FhirContext theFhirContext) {
143                return theFhirContext.getResourceTypes().stream()
144                                .flatMap(type ->
145                                                getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type).stream())
146                                .collect(Collectors.toList());
147        }
148
149        public static Set<String> getAllResourceTypesThatAreInPatientCompartment(FhirContext theFhirContext) {
150                return theFhirContext.getResourceTypes().stream()
151                                .filter(type -> getAllPatientCompartmentRuntimeSearchParamsForResourceType(theFhirContext, type)
152                                                                .size()
153                                                > 0)
154                                .collect(Collectors.toSet());
155        }
156
157        private static List<RuntimeSearchParam> getAllPatientCompartmentRuntimeSearchParams(
158                        RuntimeResourceDefinition theRuntimeResourceDefinition) {
159                List<RuntimeSearchParam> patient = theRuntimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
160                return patient;
161        }
162
163        /**
164         * Return true if any search parameter in the resource can point at a patient, false otherwise
165         */
166        public static boolean isResourceTypeInPatientCompartment(FhirContext theFhirContext, String theResourceType) {
167                RuntimeResourceDefinition runtimeResourceDefinition = theFhirContext.getResourceDefinition(theResourceType);
168                return getAllPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition)
169                                                .size()
170                                > 0;
171        }
172
173        @Nullable
174        public static String getCode(FhirContext theContext, IBaseResource theResource) {
175                return getStringChild(theContext, theResource, "code");
176        }
177
178        @Nullable
179        public static String getURL(FhirContext theContext, IBaseResource theResource) {
180                return getStringChild(theContext, theResource, "url");
181        }
182
183        @Nullable
184        public static String getExpression(FhirContext theFhirContext, IBaseResource theResource) {
185                return getStringChild(theFhirContext, theResource, "expression");
186        }
187
188        private static String getStringChild(FhirContext theFhirContext, IBaseResource theResource, String theChildName) {
189                Validate.notNull(theFhirContext, "theContext must not be null");
190                Validate.notNull(theResource, "theResource must not be null");
191                RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theResource);
192
193                BaseRuntimeChildDefinition base = def.getChildByName(theChildName);
194                return base.getAccessor()
195                                .getFirstValueOrNull(theResource)
196                                .map(t -> ((IPrimitiveType<?>) t))
197                                .map(t -> t.getValueAsString())
198                                .orElse(null);
199        }
200
201        public static String stripModifier(String theSearchParam) {
202                String retval;
203                int colonIndex = theSearchParam.indexOf(":");
204                if (colonIndex == -1) {
205                        retval = theSearchParam;
206                } else {
207                        retval = theSearchParam.substring(0, colonIndex);
208                }
209                return retval;
210        }
211}