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