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