
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.jpa.search.builder.predicate; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 024import ca.uhn.fhir.context.ConfigurationException; 025import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; 026import ca.uhn.fhir.context.RuntimeChildResourceDefinition; 027import ca.uhn.fhir.context.RuntimeResourceDefinition; 028import ca.uhn.fhir.context.RuntimeSearchParam; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.interceptor.api.HookParams; 031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 032import ca.uhn.fhir.interceptor.api.Pointcut; 033import ca.uhn.fhir.interceptor.model.RequestPartitionId; 034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 035import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 036import ca.uhn.fhir.jpa.api.dao.IDao; 037import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 038import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 039import ca.uhn.fhir.jpa.dao.BaseStorageDao; 040import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 041import ca.uhn.fhir.jpa.model.dao.JpaPid; 042import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 043import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 044import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; 045import ca.uhn.fhir.jpa.search.builder.QueryStack; 046import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams; 047import ca.uhn.fhir.jpa.search.builder.sql.ColumnTupleObject; 048import ca.uhn.fhir.jpa.search.builder.sql.JpaPidValueTuples; 049import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 050import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 051import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; 052import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 053import ca.uhn.fhir.jpa.util.QueryParameterUtils; 054import ca.uhn.fhir.model.api.IQueryParameterType; 055import ca.uhn.fhir.model.primitive.IdDt; 056import ca.uhn.fhir.parser.DataFormatException; 057import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 058import ca.uhn.fhir.rest.api.server.RequestDetails; 059import ca.uhn.fhir.rest.param.ReferenceParam; 060import ca.uhn.fhir.rest.param.TokenParam; 061import ca.uhn.fhir.rest.param.TokenParamModifier; 062import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 063import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 064import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 065import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 066import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 067import com.google.common.annotations.VisibleForTesting; 068import com.google.common.collect.Lists; 069import com.healthmarketscience.sqlbuilder.BinaryCondition; 070import com.healthmarketscience.sqlbuilder.ComboCondition; 071import com.healthmarketscience.sqlbuilder.Condition; 072import com.healthmarketscience.sqlbuilder.InCondition; 073import com.healthmarketscience.sqlbuilder.NotCondition; 074import com.healthmarketscience.sqlbuilder.SelectQuery; 075import com.healthmarketscience.sqlbuilder.UnaryCondition; 076import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 077import jakarta.annotation.Nonnull; 078import jakarta.annotation.Nullable; 079import org.apache.commons.lang3.StringUtils; 080import org.hl7.fhir.instance.model.api.IBaseResource; 081import org.hl7.fhir.instance.model.api.IIdType; 082import org.slf4j.Logger; 083import org.slf4j.LoggerFactory; 084import org.springframework.beans.factory.annotation.Autowired; 085 086import java.util.ArrayList; 087import java.util.Collection; 088import java.util.Collections; 089import java.util.HashSet; 090import java.util.List; 091import java.util.ListIterator; 092import java.util.Optional; 093import java.util.Set; 094import java.util.regex.Pattern; 095import java.util.stream.Collectors; 096 097import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with; 098import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE; 099import static ca.uhn.fhir.rest.api.Constants.VALID_MODIFIERS; 100import static org.apache.commons.lang3.StringUtils.isBlank; 101import static org.apache.commons.lang3.StringUtils.trim; 102 103public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder implements ICanMakeMissingParamPredicate { 104 105 private static final Logger ourLog = LoggerFactory.getLogger(ResourceLinkPredicateBuilder.class); 106 private static final Pattern MODIFIER_REPLACE_PATTERN = Pattern.compile(".*:"); 107 private final DbColumn myColumnSrcType; 108 private final DbColumn myColumnSrcPath; 109 private final DbColumn myColumnTargetResourceId; 110 private final DbColumn myColumnTargetResourceUrl; 111 private final DbColumn myColumnSrcResourceId; 112 private final DbColumn myColumnTargetResourceType; 113 private final QueryStack myQueryStack; 114 private final boolean myReversed; 115 116 private final DbColumn myColumnTargetPartitionId; 117 private final DbColumn myColumnSrcPartitionId; 118 119 @Autowired 120 private JpaStorageSettings myStorageSettings; 121 122 @Autowired 123 private IInterceptorBroadcaster myInterceptorBroadcaster; 124 125 @Autowired 126 private ISearchParamRegistry mySearchParamRegistry; 127 128 @Autowired 129 private IIdHelperService<JpaPid> myIdHelperService; 130 131 @Autowired 132 private DaoRegistry myDaoRegistry; 133 134 @Autowired 135 private MatchUrlService myMatchUrlService; 136 137 @Autowired 138 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 139 140 /** 141 * Constructor 142 */ 143 public ResourceLinkPredicateBuilder( 144 QueryStack theQueryStack, SearchQueryBuilder theSearchSqlBuilder, boolean theReversed) { 145 super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_RES_LINK")); 146 myColumnSrcResourceId = getTable().addColumn("SRC_RESOURCE_ID"); 147 myColumnSrcPartitionId = getTable().addColumn("PARTITION_ID"); 148 myColumnSrcType = getTable().addColumn("SOURCE_RESOURCE_TYPE"); 149 myColumnSrcPath = getTable().addColumn("SRC_PATH"); 150 myColumnTargetResourceId = getTable().addColumn("TARGET_RESOURCE_ID"); 151 myColumnTargetPartitionId = getTable().addColumn("TARGET_RES_PARTITION_ID"); 152 myColumnTargetResourceUrl = getTable().addColumn("TARGET_RESOURCE_URL"); 153 myColumnTargetResourceType = getTable().addColumn("TARGET_RESOURCE_TYPE"); 154 155 myReversed = theReversed; 156 myQueryStack = theQueryStack; 157 } 158 159 @Override 160 public DbColumn getResourceTypeColumn() { 161 if (myReversed) { 162 return myColumnTargetResourceType; 163 } else { 164 return myColumnSrcType; 165 } 166 } 167 168 public DbColumn getColumnSourcePath() { 169 return myColumnSrcPath; 170 } 171 172 public DbColumn getColumnTargetResourceId() { 173 return myColumnTargetResourceId; 174 } 175 176 public DbColumn getColumnTargetPartitionId() { 177 return myColumnTargetPartitionId; 178 } 179 180 public DbColumn[] getJoinColumnsForTarget() { 181 return getSearchQueryBuilder().toJoinColumns(getColumnTargetPartitionId(), getColumnTargetResourceId()); 182 } 183 184 public DbColumn[] getJoinColumnsForSource() { 185 return getSearchQueryBuilder().toJoinColumns(getPartitionIdColumn(), myColumnSrcResourceId); 186 } 187 188 /** 189 * Note that this may return the SRC_RESOURCE_ID or TGT_RESOURCE_ID depending 190 * on whether we're building a forward or reverse link. If you need a specific 191 * one of these, use {@link #getJoinColumnsForSource()} or {@link #getJoinColumnsForTarget()}. 192 */ 193 @Override 194 public DbColumn[] getJoinColumns() { 195 return super.getJoinColumns(); 196 } 197 198 public DbColumn getColumnSrcResourceId() { 199 return myColumnSrcResourceId; 200 } 201 202 public DbColumn getColumnSrcPartitionId() { 203 return myColumnSrcPartitionId; 204 } 205 206 public DbColumn getColumnTargetResourceType() { 207 return myColumnTargetResourceType; 208 } 209 210 @Override 211 public DbColumn getResourceIdColumn() { 212 if (myReversed) { 213 return myColumnTargetResourceId; 214 } else { 215 return myColumnSrcResourceId; 216 } 217 } 218 219 @Nullable 220 public Condition createPredicate( 221 RequestDetails theRequest, 222 String theResourceType, 223 String theParamName, 224 List<String> theQualifiers, 225 List<? extends IQueryParameterType> theReferenceOrParamList, 226 SearchFilterParser.CompareOperation theOperation, 227 RequestPartitionId theRequestPartitionId) { 228 229 List<IIdType> targetIds = new ArrayList<>(); 230 List<String> targetQualifiedUrls = new ArrayList<>(); 231 232 /* 233 * If we're allowing cross-partition references, we should first figure out what partition the 234 * link search should be able to touch 235 */ 236 RequestPartitionId requestPartitionId = theRequestPartitionId; 237 if (myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()) { 238 SearchParameterMap map = new SearchParameterMap(); 239 map.addOrList(theParamName, theReferenceOrParamList); 240 requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( 241 theRequest, theResourceType, map); 242 } 243 244 for (int orIdx = 0; orIdx < theReferenceOrParamList.size(); orIdx++) { 245 IQueryParameterType nextOr = theReferenceOrParamList.get(orIdx); 246 247 if (nextOr instanceof ReferenceParam) { 248 ReferenceParam ref = (ReferenceParam) nextOr; 249 250 if (isBlank(ref.getChain())) { 251 252 /* 253 * Handle non-chained search, e.g. Patient?organization=Organization/123 254 */ 255 256 IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null); 257 258 if (dt.hasBaseUrl()) { 259 if (myStorageSettings.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { 260 dt = dt.toUnqualified(); 261 targetIds.add(dt); 262 } else { 263 targetQualifiedUrls.add(dt.getValue()); 264 } 265 } else { 266 validateModifierUse(theRequest, theResourceType, ref); 267 validateResourceTypeInReferenceParam(ref.getResourceType()); 268 targetIds.add(dt); 269 } 270 271 } else { 272 273 /* 274 * Handle chained search, e.g. Patient?organization.name=Kwik-e-mart 275 */ 276 277 return addPredicateReferenceWithChain( 278 theResourceType, 279 theParamName, 280 theQualifiers, 281 theReferenceOrParamList, 282 ref, 283 theRequest, 284 requestPartitionId); 285 } 286 287 } else { 288 throw new IllegalArgumentException( 289 Msg.code(1241) + "Invalid token type (expecting ReferenceParam): " + nextOr.getClass()); 290 } 291 } 292 293 for (IIdType next : targetIds) { 294 if (!next.hasResourceType()) { 295 warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, null); 296 } 297 } 298 299 List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName, theQualifiers); 300 boolean inverse; 301 if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) { 302 inverse = false; 303 } else { 304 inverse = true; 305 } 306 307 List<JpaPid> pids = myIdHelperService.resolveResourcePids( 308 requestPartitionId, 309 targetIds, 310 ResolveIdentityMode.includeDeleted().cacheOk()); 311 List<Long> targetPidList = pids.stream().map(JpaPid::getId).collect(Collectors.toList()); 312 313 if (targetPidList.isEmpty() && targetQualifiedUrls.isEmpty()) { 314 setMatchNothing(); 315 return null; 316 } else { 317 Condition retVal = createPredicateReference(inverse, pathsToMatch, targetPidList, targetQualifiedUrls); 318 return combineWithRequestPartitionIdPredicate(getRequestPartitionId(), retVal); 319 } 320 } 321 322 private void validateModifierUse(RequestDetails theRequest, String theResourceType, ReferenceParam theRef) { 323 try { 324 final String resourceTypeFromRef = theRef.getResourceType(); 325 if (StringUtils.isEmpty(resourceTypeFromRef)) { 326 return; 327 } 328 // TODO: LD: unless we do this, ResourceProviderR4Test#testSearchWithSlashes will fail due to its 329 // derived-from: syntax 330 getFhirContext().getResourceDefinition(resourceTypeFromRef); 331 } catch (DataFormatException e) { 332 final List<String> nonMatching = Optional.ofNullable(theRequest) 333 .map(RequestDetails::getParameters) 334 .map(params -> params.keySet().stream() 335 .filter(mod -> mod.contains(":")) 336 .map(MODIFIER_REPLACE_PATTERN::matcher) 337 .map(pattern -> pattern.replaceAll(":")) 338 .filter(mod -> !VALID_MODIFIERS.contains(mod)) 339 .distinct() 340 .collect(Collectors.toUnmodifiableList())) 341 .orElse(Collections.emptyList()); 342 343 if (!nonMatching.isEmpty()) { 344 final String msg = getFhirContext() 345 .getLocalizer() 346 .getMessageSanitized( 347 SearchCoordinatorSvcImpl.class, 348 "invalidUseOfSearchIdentifier", 349 nonMatching, 350 theResourceType, 351 VALID_MODIFIERS); 352 throw new InvalidRequestException(Msg.code(2498) + msg); 353 } 354 } 355 } 356 357 private void validateResourceTypeInReferenceParam(final String theResourceType) { 358 if (StringUtils.isEmpty(theResourceType)) { 359 return; 360 } 361 362 try { 363 getFhirContext().getResourceDefinition(theResourceType); 364 } catch (DataFormatException e) { 365 throw newInvalidResourceTypeException(theResourceType); 366 } 367 } 368 369 private Condition createPredicateReference( 370 boolean theInverse, 371 List<String> thePathsToMatch, 372 List<Long> theTargetPidList, 373 List<String> theTargetQualifiedUrls) { 374 375 Condition targetPidCondition = null; 376 if (!theTargetPidList.isEmpty()) { 377 List<String> placeholders = generatePlaceholders(theTargetPidList); 378 targetPidCondition = 379 QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceId, placeholders, theInverse); 380 } 381 382 Condition targetUrlsCondition = null; 383 if (!theTargetQualifiedUrls.isEmpty()) { 384 List<String> placeholders = generatePlaceholders(theTargetQualifiedUrls); 385 targetUrlsCondition = 386 QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceUrl, placeholders, theInverse); 387 } 388 389 Condition joinedCondition; 390 if (targetPidCondition != null && targetUrlsCondition != null) { 391 joinedCondition = ComboCondition.or(targetPidCondition, targetUrlsCondition); 392 } else if (targetPidCondition != null) { 393 joinedCondition = targetPidCondition; 394 } else { 395 joinedCondition = targetUrlsCondition; 396 } 397 398 Condition pathPredicate = createPredicateSourcePaths(thePathsToMatch); 399 joinedCondition = ComboCondition.and(pathPredicate, joinedCondition); 400 401 return joinedCondition; 402 } 403 404 @Nonnull 405 public Condition createPredicateSourcePaths(List<String> thePathsToMatch) { 406 return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); 407 } 408 409 public Condition createPredicateSourcePaths(String theResourceName, String theParamName) { 410 List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, Collections.emptyList()); 411 return createPredicateSourcePaths(pathsToMatch); 412 } 413 414 private void warnAboutPerformanceOnUnqualifiedResources( 415 String theParamName, RequestDetails theRequest, @Nullable List<String> theCandidateTargetTypes) { 416 StringBuilder builder = new StringBuilder(); 417 builder.append("This search uses an unqualified resource(a parameter in a chain without a resource type). "); 418 builder.append("This is less efficient than using a qualified type. "); 419 if (theCandidateTargetTypes != null) { 420 builder.append("[" + theParamName + "] resolves to [" 421 + theCandidateTargetTypes.stream().collect(Collectors.joining(",")) + "]."); 422 builder.append("If you know what you're looking for, try qualifying it using the form "); 423 builder.append(theCandidateTargetTypes.stream() 424 .map(cls -> "[" + cls + ":" + theParamName + "]") 425 .collect(Collectors.joining(" or "))); 426 } else { 427 builder.append("If you know what you're looking for, try qualifying it using the form: '"); 428 builder.append(theParamName).append(":[resourceType]=[id] or "); 429 builder.append(theParamName).append("=[resourceType]/[id]"); 430 builder.append("'"); 431 } 432 String message = builder.toString(); 433 StorageProcessingMessage msg = new StorageProcessingMessage().setMessage(message); 434 435 IInterceptorBroadcaster compositeBroadcaster = 436 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 437 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { 438 HookParams params = new HookParams() 439 .add(RequestDetails.class, theRequest) 440 .addIfMatchesType(ServletRequestDetails.class, theRequest) 441 .add(StorageProcessingMessage.class, msg); 442 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 443 } 444 } 445 446 /** 447 * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain 448 * on the device. 449 */ 450 @Nullable 451 private Condition addPredicateReferenceWithChain( 452 String theResourceName, 453 String theParamName, 454 List<String> theQualifiers, 455 List<? extends IQueryParameterType> theList, 456 ReferenceParam theReferenceParam, 457 RequestDetails theRequest, 458 RequestPartitionId theRequestPartitionId) { 459 460 /* 461 * Which resource types can the given chained parameter actually link to? This might be a list 462 * where the chain is unqualified, as in: Observation?subject.identifier=(...) 463 * since subject can link to several possible target types. 464 * 465 * If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...) 466 * this is just a simple 1-entry list. 467 */ 468 final List<String> resourceTypes = 469 determineCandidateResourceTypesForChain(theResourceName, theParamName, theReferenceParam); 470 471 /* 472 * Handle chain on _type 473 */ 474 if (PARAM_TYPE.equals(theReferenceParam.getChain())) { 475 476 List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers); 477 Condition typeCondition = createPredicateSourcePaths(pathsToMatch); 478 479 String typeValue = theReferenceParam.getValue(); 480 481 validateResourceTypeInReferenceParam(typeValue); 482 if (!resourceTypes.contains(typeValue)) { 483 throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue); 484 } 485 486 Condition condition = BinaryCondition.equalTo( 487 myColumnTargetResourceType, generatePlaceholder(theReferenceParam.getValue())); 488 489 return QueryParameterUtils.toAndPredicate(typeCondition, condition); 490 } 491 492 boolean foundChainMatch = false; 493 List<String> candidateTargetTypes = new ArrayList<>(); 494 List<Condition> orPredicates = new ArrayList<>(); 495 boolean paramInverted = false; 496 QueryStack childQueryFactory = myQueryStack.newChildQueryFactoryWithFullBuilderReuse(); 497 498 String chain = theReferenceParam.getChain(); 499 500 String remainingChain = null; 501 int chainDotIndex = chain.indexOf('.'); 502 if (chainDotIndex != -1) { 503 remainingChain = chain.substring(chainDotIndex + 1); 504 chain = chain.substring(0, chainDotIndex); 505 } 506 507 int qualifierIndex = chain.indexOf(':'); 508 String qualifier = null; 509 if (qualifierIndex != -1) { 510 qualifier = chain.substring(qualifierIndex); 511 chain = chain.substring(0, qualifierIndex); 512 } 513 514 boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain); 515 516 for (String nextType : resourceTypes) { 517 518 RuntimeResourceDefinition typeDef = getFhirContext().getResourceDefinition(nextType); 519 String subResourceName = typeDef.getName(); 520 521 IDao dao = myDaoRegistry.getResourceDao(nextType); 522 if (dao == null) { 523 ourLog.debug("Don't have a DAO for type {}", nextType); 524 continue; 525 } 526 527 RuntimeSearchParam param = null; 528 if (!isMeta) { 529 param = mySearchParamRegistry.getActiveSearchParam( 530 nextType, chain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 531 if (param == null) { 532 ourLog.debug("Type {} doesn't have search param {}", nextType, param); 533 continue; 534 } 535 } 536 537 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 538 539 for (IQueryParameterType next : theList) { 540 String nextValue = next.getValueAsQueryToken(); 541 IQueryParameterType chainValue = mapReferenceChainToRawParamType( 542 remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue); 543 if (chainValue == null) { 544 continue; 545 } 546 547 // For the token param, if it's a :not modifier, need switch OR to AND 548 if (!paramInverted && chainValue instanceof TokenParam) { 549 if (((TokenParam) chainValue).getModifier() == TokenParamModifier.NOT) { 550 paramInverted = true; 551 } 552 } 553 foundChainMatch = true; 554 orValues.add(chainValue); 555 } 556 557 if (!foundChainMatch) { 558 throw new InvalidRequestException(Msg.code(1242) 559 + getFhirContext() 560 .getLocalizer() 561 .getMessage( 562 BaseStorageDao.class, 563 "invalidParameterChain", 564 theParamName + '.' + theReferenceParam.getChain())); 565 } 566 567 candidateTargetTypes.add(nextType); 568 569 List<Condition> andPredicates = new ArrayList<>(); 570 571 List<List<IQueryParameterType>> chainParamValues = Collections.singletonList(orValues); 572 andPredicates.add( 573 childQueryFactory.searchForIdsWithAndOr(with().setSourceJoinColumn(getJoinColumnsForTarget()) 574 .setResourceName(subResourceName) 575 .setParamName(chain) 576 .setAndOrParams(chainParamValues) 577 .setRequest(theRequest) 578 .setRequestPartitionId(theRequestPartitionId))); 579 580 orPredicates.add(QueryParameterUtils.toAndPredicate(andPredicates)); 581 } 582 583 if (candidateTargetTypes.isEmpty()) { 584 throw new InvalidRequestException(Msg.code(1243) 585 + getFhirContext() 586 .getLocalizer() 587 .getMessage( 588 BaseStorageDao.class, 589 "invalidParameterChain", 590 theParamName + '.' + theReferenceParam.getChain())); 591 } 592 593 if (candidateTargetTypes.size() > 1) { 594 warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes); 595 } 596 597 // If :not modifier for a token, switch OR with AND in the multi-type case 598 Condition multiTypePredicate; 599 if (paramInverted) { 600 multiTypePredicate = QueryParameterUtils.toAndPredicate(orPredicates); 601 } else { 602 multiTypePredicate = QueryParameterUtils.toOrPredicate(orPredicates); 603 } 604 605 List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers); 606 Condition pathPredicate = createPredicateSourcePaths(pathsToMatch); 607 return QueryParameterUtils.toAndPredicate(pathPredicate, multiTypePredicate); 608 } 609 610 @Nonnull 611 private List<String> determineCandidateResourceTypesForChain( 612 String theResourceName, String theParamName, ReferenceParam theReferenceParam) { 613 final List<Class<? extends IBaseResource>> resourceTypes; 614 if (!theReferenceParam.hasResourceType()) { 615 616 resourceTypes = determineResourceTypes(Collections.singleton(theResourceName), theParamName); 617 618 if (resourceTypes.isEmpty()) { 619 RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam( 620 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 621 if (searchParamByName == null) { 622 throw new InternalErrorException(Msg.code(1244) + "Could not find parameter " + theParamName); 623 } 624 String paramPath = searchParamByName.getPath(); 625 if (paramPath.endsWith(".as(Reference)")) { 626 paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference"; 627 } 628 629 if (paramPath.contains(".extension(")) { 630 int startIdx = paramPath.indexOf(".extension("); 631 int endIdx = paramPath.indexOf(')', startIdx); 632 if (startIdx != -1 && endIdx != -1) { 633 paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1); 634 } 635 } 636 637 Class<? extends IBaseResource> resourceType = 638 getFhirContext().getResourceDefinition(theResourceName).getImplementingClass(); 639 BaseRuntimeChildDefinition def = getFhirContext().newTerser().getDefinition(resourceType, paramPath); 640 if (def instanceof RuntimeChildChoiceDefinition choiceDef) { 641 resourceTypes.addAll(choiceDef.getResourceTypes()); 642 } else if (def instanceof RuntimeChildResourceDefinition resDef) { 643 resourceTypes.addAll(resDef.getResourceTypes()); 644 if (resourceTypes.size() == 1) { 645 if (resourceTypes.get(0).isInterface()) { 646 throw new InvalidRequestException( 647 Msg.code(1245) + "Unable to perform search for unqualified chain '" + theParamName 648 + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" 649 + theParamName + ":[ResourceType]' to perform this search."); 650 } 651 } 652 } else { 653 throw new ConfigurationException(Msg.code(1246) + "Property " + paramPath + " of type " 654 + getResourceType() + " is not a resource: " + def.getClass()); 655 } 656 } 657 658 if (resourceTypes.isEmpty()) { 659 for (BaseRuntimeElementDefinition<?> next : getFhirContext().getElementDefinitions()) { 660 if (next instanceof RuntimeResourceDefinition nextResDef) { 661 resourceTypes.add(nextResDef.getImplementingClass()); 662 } 663 } 664 } 665 666 } else { 667 668 try { 669 RuntimeResourceDefinition resDef = 670 getFhirContext().getResourceDefinition(theReferenceParam.getResourceType()); 671 resourceTypes = new ArrayList<>(1); 672 resourceTypes.add(resDef.getImplementingClass()); 673 } catch (DataFormatException e) { 674 throw newInvalidResourceTypeException(theReferenceParam.getResourceType()); 675 } 676 } 677 678 return resourceTypes.stream() 679 .map(t -> getFhirContext().getResourceType(t)) 680 .collect(Collectors.toList()); 681 } 682 683 private List<Class<? extends IBaseResource>> determineResourceTypes( 684 Set<String> theResourceNames, String theParamNameChain) { 685 int linkIndex = theParamNameChain.indexOf('.'); 686 if (linkIndex == -1) { 687 Set<Class<? extends IBaseResource>> resourceTypes = new HashSet<>(); 688 for (String resourceName : theResourceNames) { 689 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 690 resourceName, theParamNameChain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 691 692 if (param != null && param.hasTargets()) { 693 Set<String> targetTypes = param.getTargets(); 694 for (String next : targetTypes) { 695 resourceTypes.add( 696 getFhirContext().getResourceDefinition(next).getImplementingClass()); 697 } 698 } 699 } 700 return new ArrayList<>(resourceTypes); 701 } else { 702 String paramNameHead = theParamNameChain.substring(0, linkIndex); 703 String paramNameTail = theParamNameChain.substring(linkIndex + 1); 704 Set<String> targetResourceTypeNames = new HashSet<>(); 705 for (String resourceName : theResourceNames) { 706 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 707 resourceName, paramNameHead, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 708 709 if (param != null && param.hasTargets()) { 710 targetResourceTypeNames.addAll(param.getTargets()); 711 } 712 } 713 return determineResourceTypes(targetResourceTypeNames, paramNameTail); 714 } 715 } 716 717 public List<String> createResourceLinkPaths( 718 String theResourceName, String theParamName, List<String> theParamQualifiers) { 719 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 720 theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 721 if (param != null) { 722 List<String> path = param.getPathsSplit(); 723 724 /* 725 * SearchParameters can declare paths on multiple resource 726 * types. Here we only want the ones that actually apply. 727 */ 728 path = new ArrayList<>(path); 729 730 ListIterator<String> iter = path.listIterator(); 731 while (iter.hasNext()) { 732 String nextPath = trim(iter.next()); 733 if (!nextPath.contains(theResourceName + ".")) { 734 iter.remove(); 735 } 736 } 737 738 return path; 739 } 740 741 boolean containsChain = theParamName.contains("."); 742 if (containsChain) { 743 int linkIndex = theParamName.indexOf('.'); 744 String paramNameHead = theParamName.substring(0, linkIndex); 745 String paramNameTail = theParamName.substring(linkIndex + 1); 746 String qualifier = !theParamQualifiers.isEmpty() ? theParamQualifiers.get(0) : null; 747 List<String> nextQualifiersList = !theParamQualifiers.isEmpty() 748 ? theParamQualifiers.subList(1, theParamQualifiers.size()) 749 : List.of(); 750 751 param = mySearchParamRegistry.getActiveSearchParam( 752 theResourceName, paramNameHead, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 753 if (param != null) { 754 Set<String> tailPaths = param.getTargets().stream() 755 .filter(t -> isBlank(qualifier) || qualifier.equals(t)) 756 .map(t -> createResourceLinkPaths(t, paramNameTail, nextQualifiersList)) 757 .flatMap(Collection::stream) 758 .map(t -> t.substring(t.indexOf('.') + 1)) 759 .collect(Collectors.toSet()); 760 761 List<String> path = param.getPathsSplit(); 762 763 /* 764 * SearchParameters can declare paths on multiple resource 765 * types. Here we only want the ones that actually apply. 766 * Then append all the tail paths to each of the applicable head paths 767 */ 768 return path.stream() 769 .map(String::trim) 770 .filter(t -> t.startsWith(theResourceName + ".")) 771 .map(head -> tailPaths.stream() 772 .map(tail -> head + "." + tail) 773 .collect(Collectors.toSet())) 774 .flatMap(Collection::stream) 775 .collect(Collectors.toList()); 776 } 777 } 778 779 // This can happen during recursion, if not all the possible target types of one link in the chain 780 // support the next link 781 return new ArrayList<>(); 782 } 783 784 private IQueryParameterType mapReferenceChainToRawParamType( 785 String remainingChain, 786 RuntimeSearchParam param, 787 String theParamName, 788 String qualifier, 789 String nextType, 790 String chain, 791 boolean isMeta, 792 String resourceId) { 793 IQueryParameterType chainValue; 794 if (remainingChain != null) { 795 if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { 796 ourLog.debug( 797 "Type {} parameter {} is not a reference, can not chain {}", nextType, chain, remainingChain); 798 return null; 799 } 800 801 chainValue = new ReferenceParam(); 802 chainValue.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId); 803 ((ReferenceParam) chainValue).setChain(remainingChain); 804 } else if (isMeta) { 805 IQueryParameterType type = myMatchUrlService.newInstanceType(chain); 806 type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId); 807 chainValue = type; 808 } else { 809 chainValue = myQueryStack.newParameterInstance(param, qualifier, resourceId); 810 } 811 812 return chainValue; 813 } 814 815 @Nonnull 816 private InvalidRequestException newInvalidTargetTypeForChainException( 817 String theResourceName, String theParamName, String theTypeValue) { 818 String searchParamName = theResourceName + ":" + theParamName; 819 String msg = getFhirContext() 820 .getLocalizer() 821 .getMessage( 822 ResourceLinkPredicateBuilder.class, "invalidTargetTypeForChain", theTypeValue, searchParamName); 823 return new InvalidRequestException(Msg.code(2495) + msg); 824 } 825 826 @Nonnull 827 private InvalidRequestException newInvalidResourceTypeException(String theResourceType) { 828 String msg = getFhirContext() 829 .getLocalizer() 830 .getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType); 831 throw new InvalidRequestException(Msg.code(1250) + msg); 832 } 833 834 @Nonnull 835 public Condition createEverythingPredicate( 836 String theResourceName, List<String> theSourceResourceNames, JpaPid... theTargetPids) { 837 Condition condition; 838 839 if (theTargetPids != null && theTargetPids.length >= 1) { 840 // if resource ids are provided, we'll create the predicate 841 // with ids in or equal to this value 842 if (getSearchQueryBuilder().isIncludePartitionIdInJoins()) { 843 Object left = ColumnTupleObject.from(getJoinColumnsForTarget()); 844 JpaPidValueTuples right = JpaPidValueTuples.from(getSearchQueryBuilder(), theTargetPids); 845 condition = new InCondition(left, right); 846 } else { 847 condition = QueryParameterUtils.toEqualToOrInPredicate( 848 myColumnTargetResourceId, generatePlaceholders(JpaPid.toLongList(theTargetPids))); 849 } 850 } else { 851 // ... otherwise we look for resource types 852 condition = BinaryCondition.equalTo(myColumnTargetResourceType, generatePlaceholder(theResourceName)); 853 } 854 855 if (!theSourceResourceNames.isEmpty()) { 856 // if source resources are provided, add on predicate for _type operation 857 Condition typeCondition = QueryParameterUtils.toEqualToOrInPredicate( 858 myColumnSrcType, generatePlaceholders(theSourceResourceNames)); 859 condition = QueryParameterUtils.toAndPredicate(List.of(condition, typeCondition)); 860 } 861 862 return condition; 863 } 864 865 @Override 866 public Condition createPredicateParamMissingValue(MissingQueryParameterPredicateParams theParams) { 867 SelectQuery subquery = new SelectQuery(); 868 subquery.addCustomColumns(1); 869 subquery.addFromTable(getTable()); 870 871 String resourceType = theParams.getResourceTablePredicateBuilder().getResourceType(); 872 RuntimeSearchParam paramDefinition = mySearchParamRegistry.getRuntimeSearchParam( 873 resourceType, theParams.getParamName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 874 List<String> pathList = paramDefinition.getPathsSplitForResourceType(resourceType); 875 876 Condition subQueryCondition = ComboCondition.and( 877 BinaryCondition.equalTo( 878 getResourceIdColumn(), 879 theParams.getResourceTablePredicateBuilder().getResourceIdColumn()), 880 BinaryCondition.equalTo(getResourceTypeColumn(), generatePlaceholder(resourceType)), 881 ComboCondition.or(pathList.stream() 882 .map(path -> BinaryCondition.equalTo(getColumnSourcePath(), generatePlaceholder(path))) 883 .toArray(BinaryCondition[]::new))); 884 885 subquery.addCondition(subQueryCondition); 886 887 Condition unaryCondition = UnaryCondition.exists(subquery); 888 if (theParams.isMissing()) { 889 unaryCondition = new NotCondition(unaryCondition); 890 } 891 892 return combineWithRequestPartitionIdPredicate(theParams.getRequestPartitionId(), unaryCondition); 893 } 894 895 @VisibleForTesting 896 void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) { 897 mySearchParamRegistry = theSearchParamRegistry; 898 } 899 900 @VisibleForTesting 901 void setIdHelperServiceForUnitTest(IIdHelperService theIdHelperService) { 902 myIdHelperService = theIdHelperService; 903 } 904}