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