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