
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}