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}