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