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