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; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.exception.TokenParamFormatInvalidRequestException; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.dao.BaseStorageDao; 029import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 030import ca.uhn.fhir.jpa.model.config.PartitionSettings; 031import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; 032import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 033import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; 034import ca.uhn.fhir.jpa.search.builder.models.MissingParameterQueryParams; 035import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams; 036import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheKey; 037import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheLookupResult; 038import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderTypeEnum; 039import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder; 040import ca.uhn.fhir.jpa.search.builder.predicate.BaseQuantityPredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.BaseSearchParamPredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; 047import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate; 048import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; 049import ca.uhn.fhir.jpa.search.builder.predicate.ParsedLocationParam; 050import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 051import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 052import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 053import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 054import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder; 055import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 056import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 057import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 058import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 059import ca.uhn.fhir.jpa.search.builder.sql.PredicateBuilderFactory; 060import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 061import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 062import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; 063import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 064import ca.uhn.fhir.jpa.searchparam.util.SourceParam; 065import ca.uhn.fhir.model.api.IQueryParameterAnd; 066import ca.uhn.fhir.model.api.IQueryParameterOr; 067import ca.uhn.fhir.model.api.IQueryParameterType; 068import ca.uhn.fhir.parser.DataFormatException; 069import ca.uhn.fhir.rest.api.Constants; 070import ca.uhn.fhir.rest.api.QualifiedParamList; 071import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 072import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 073import ca.uhn.fhir.rest.api.server.RequestDetails; 074import ca.uhn.fhir.rest.param.CompositeParam; 075import ca.uhn.fhir.rest.param.DateParam; 076import ca.uhn.fhir.rest.param.HasParam; 077import ca.uhn.fhir.rest.param.NumberParam; 078import ca.uhn.fhir.rest.param.QuantityParam; 079import ca.uhn.fhir.rest.param.ReferenceParam; 080import ca.uhn.fhir.rest.param.SpecialParam; 081import ca.uhn.fhir.rest.param.StringParam; 082import ca.uhn.fhir.rest.param.TokenParam; 083import ca.uhn.fhir.rest.param.TokenParamModifier; 084import ca.uhn.fhir.rest.param.UriParam; 085import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 086import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 087import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 088import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 089import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 090import com.google.common.collect.Lists; 091import com.google.common.collect.Maps; 092import com.google.common.collect.Sets; 093import com.healthmarketscience.sqlbuilder.BinaryCondition; 094import com.healthmarketscience.sqlbuilder.ComboCondition; 095import com.healthmarketscience.sqlbuilder.Condition; 096import com.healthmarketscience.sqlbuilder.Expression; 097import com.healthmarketscience.sqlbuilder.InCondition; 098import com.healthmarketscience.sqlbuilder.OrderObject; 099import com.healthmarketscience.sqlbuilder.SelectQuery; 100import com.healthmarketscience.sqlbuilder.SetOperationQuery; 101import com.healthmarketscience.sqlbuilder.Subquery; 102import com.healthmarketscience.sqlbuilder.UnionQuery; 103import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 104import org.apache.commons.lang3.StringUtils; 105import org.apache.commons.lang3.tuple.Triple; 106import org.hl7.fhir.instance.model.api.IAnyResource; 107import org.slf4j.Logger; 108import org.slf4j.LoggerFactory; 109import org.springframework.util.CollectionUtils; 110 111import java.math.BigDecimal; 112import java.util.ArrayList; 113import java.util.Collection; 114import java.util.Collections; 115import java.util.EnumSet; 116import java.util.HashMap; 117import java.util.List; 118import java.util.Map; 119import java.util.Objects; 120import java.util.Optional; 121import java.util.Set; 122import java.util.function.Supplier; 123import java.util.stream.Collectors; 124import javax.annotation.Nullable; 125 126import static ca.uhn.fhir.jpa.util.QueryParameterUtils.fromOperation; 127import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getChainedPart; 128import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getParamNameWithPrefix; 129import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate; 130import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate; 131import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation; 132import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate; 133import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS; 134import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; 135import static org.apache.commons.lang3.StringUtils.isBlank; 136import static org.apache.commons.lang3.StringUtils.isNotBlank; 137import static org.apache.commons.lang3.StringUtils.split; 138 139public class QueryStack { 140 141 private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class); 142 public static final String LOCATION_POSITION = "Location.position"; 143 144 private final FhirContext myFhirContext; 145 private final SearchQueryBuilder mySqlBuilder; 146 private final SearchParameterMap mySearchParameters; 147 private final ISearchParamRegistry mySearchParamRegistry; 148 private final PartitionSettings myPartitionSettings; 149 private final JpaStorageSettings myStorageSettings; 150 private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes; 151 private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap; 152 private Map<String, BaseJoiningPredicateBuilder> myParamNameToPredicateBuilderMap; 153 // used for _offset queries with sort, should be removed once the fix is applied to the async path too. 154 private boolean myUseAggregate; 155 156 /** 157 * Constructor 158 */ 159 public QueryStack( 160 SearchParameterMap theSearchParameters, 161 JpaStorageSettings theStorageSettings, 162 FhirContext theFhirContext, 163 SearchQueryBuilder theSqlBuilder, 164 ISearchParamRegistry theSearchParamRegistry, 165 PartitionSettings thePartitionSettings) { 166 this( 167 theSearchParameters, 168 theStorageSettings, 169 theFhirContext, 170 theSqlBuilder, 171 theSearchParamRegistry, 172 thePartitionSettings, 173 EnumSet.of(PredicateBuilderTypeEnum.DATE)); 174 } 175 176 /** 177 * Constructor 178 */ 179 private QueryStack( 180 SearchParameterMap theSearchParameters, 181 JpaStorageSettings theStorageSettings, 182 FhirContext theFhirContext, 183 SearchQueryBuilder theSqlBuilder, 184 ISearchParamRegistry theSearchParamRegistry, 185 PartitionSettings thePartitionSettings, 186 EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) { 187 myPartitionSettings = thePartitionSettings; 188 assert theSearchParameters != null; 189 assert theStorageSettings != null; 190 assert theFhirContext != null; 191 assert theSqlBuilder != null; 192 193 mySearchParameters = theSearchParameters; 194 myStorageSettings = theStorageSettings; 195 myFhirContext = theFhirContext; 196 mySqlBuilder = theSqlBuilder; 197 mySearchParamRegistry = theSearchParamRegistry; 198 myReusePredicateBuilderTypes = theReusePredicateBuilderTypes; 199 } 200 201 public void addSortOnCoordsNear(String theParamName, boolean theAscending, SearchParameterMap theParams) { 202 boolean handled = false; 203 if (myParamNameToPredicateBuilderMap != null) { 204 BaseJoiningPredicateBuilder builder = myParamNameToPredicateBuilderMap.get(theParamName); 205 if (builder instanceof CoordsPredicateBuilder) { 206 CoordsPredicateBuilder coordsBuilder = (CoordsPredicateBuilder) builder; 207 208 List<List<IQueryParameterType>> params = theParams.get(theParamName); 209 if (params.size() > 0 && params.get(0).size() > 0) { 210 IQueryParameterType param = params.get(0).get(0); 211 ParsedLocationParam location = ParsedLocationParam.from(theParams, param); 212 double latitudeValue = location.getLatitudeValue(); 213 double longitudeValue = location.getLongitudeValue(); 214 mySqlBuilder.addSortCoordsNear(coordsBuilder, latitudeValue, longitudeValue, theAscending); 215 handled = true; 216 } 217 } 218 } 219 220 if (!handled) { 221 String msg = myFhirContext 222 .getLocalizer() 223 .getMessageSanitized(QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName); 224 throw new InvalidRequestException(Msg.code(2307) + msg); 225 } 226 } 227 228 public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) { 229 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 230 DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder(); 231 232 Condition hashIdentityPredicate = 233 datePredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 234 235 addSortCustomJoin(firstPredicateBuilder, datePredicateBuilder, hashIdentityPredicate); 236 237 mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate); 238 } 239 240 public void addSortOnLastUpdated(boolean theAscending) { 241 ResourceTablePredicateBuilder resourceTablePredicateBuilder; 242 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 243 if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) { 244 resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder; 245 } else { 246 resourceTablePredicateBuilder = 247 mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 248 } 249 mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending, myUseAggregate); 250 } 251 252 public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) { 253 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 254 NumberPredicateBuilder numberPredicateBuilder = mySqlBuilder.createNumberPredicateBuilder(); 255 256 Condition hashIdentityPredicate = 257 numberPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 258 259 addSortCustomJoin(firstPredicateBuilder, numberPredicateBuilder, hashIdentityPredicate); 260 261 mySqlBuilder.addSortNumeric(numberPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 262 } 263 264 public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) { 265 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 266 267 BaseQuantityPredicateBuilder quantityPredicateBuilder = mySqlBuilder.createQuantityPredicateBuilder(); 268 269 Condition hashIdentityPredicate = 270 quantityPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 271 272 addSortCustomJoin(firstPredicateBuilder, quantityPredicateBuilder, hashIdentityPredicate); 273 274 mySqlBuilder.addSortNumeric(quantityPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 275 } 276 277 public void addSortOnResourceId(boolean theAscending) { 278 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 279 ForcedIdPredicateBuilder sortPredicateBuilder = 280 mySqlBuilder.addForcedIdPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 281 if (!theAscending) { 282 mySqlBuilder.addSortString( 283 sortPredicateBuilder.getColumnForcedId(), false, OrderObject.NullOrder.FIRST, myUseAggregate); 284 } else { 285 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnForcedId(), true, myUseAggregate); 286 } 287 mySqlBuilder.addSortNumeric(firstPredicateBuilder.getResourceIdColumn(), theAscending, myUseAggregate); 288 } 289 290 public void addSortOnResourceLink( 291 String theResourceName, 292 String theReferenceTargetType, 293 String theParamName, 294 String theChain, 295 boolean theAscending) { 296 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 297 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this); 298 299 Condition pathPredicate = 300 resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName); 301 302 addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate); 303 304 if (isBlank(theChain)) { 305 mySqlBuilder.addSortNumeric( 306 resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate); 307 return; 308 } 309 310 String targetType = null; 311 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); 312 if (theReferenceTargetType != null) { 313 targetType = theReferenceTargetType; 314 } else if (param.getTargets().size() > 1) { 315 throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '" 316 + theParamName + "' as this parameter has multiple target types. Please specify the target type."); 317 } else if (param.getTargets().size() == 1) { 318 targetType = param.getTargets().iterator().next(); 319 } 320 321 if (isBlank(targetType)) { 322 throw new InvalidRequestException( 323 Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName 324 + "' as this parameter as this parameter does not define a target type. Please specify the target type."); 325 } 326 327 RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam(targetType, theChain); 328 if (targetSearchParameter == null) { 329 Collection<String> validSearchParameterNames = 330 mySearchParamRegistry.getActiveSearchParams(targetType).values().stream() 331 .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.STRING 332 || t.getParamType() == RestSearchParameterTypeEnum.TOKEN 333 || t.getParamType() == RestSearchParameterTypeEnum.DATE) 334 .map(RuntimeSearchParam::getName) 335 .sorted() 336 .distinct() 337 .collect(Collectors.toList()); 338 String msg = myFhirContext 339 .getLocalizer() 340 .getMessageSanitized( 341 BaseStorageDao.class, 342 "invalidSortParameter", 343 theChain, 344 targetType, 345 validSearchParameterNames); 346 throw new InvalidRequestException(Msg.code(2289) + msg); 347 } 348 349 BaseSearchParamPredicateBuilder chainedPredicateBuilder; 350 DbColumn[] sortColumn; 351 switch (targetSearchParameter.getParamType()) { 352 case STRING: 353 StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder(); 354 sortColumn = new DbColumn[] {stringPredicateBuilder.getColumnValueNormalized()}; 355 chainedPredicateBuilder = stringPredicateBuilder; 356 break; 357 case TOKEN: 358 TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder(); 359 sortColumn = 360 new DbColumn[] {tokenPredicateBuilder.getColumnSystem(), tokenPredicateBuilder.getColumnValue() 361 }; 362 chainedPredicateBuilder = tokenPredicateBuilder; 363 break; 364 case DATE: 365 DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder(); 366 sortColumn = new DbColumn[] {datePredicateBuilder.getColumnValueLow()}; 367 chainedPredicateBuilder = datePredicateBuilder; 368 break; 369 370 /* 371 * Note that many of the options below aren't implemented because they 372 * don't seem useful to me, but they could theoretically be implemented 373 * if someone ever needed them. I'm not sure why you'd want to do a chained 374 * sort on a target that was a reference or a quantity, but if someone needed 375 * that we could implement it here. 376 */ 377 case NUMBER: 378 case REFERENCE: 379 case COMPOSITE: 380 case QUANTITY: 381 case URI: 382 case HAS: 383 case SPECIAL: 384 default: 385 throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter " 386 + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: " 387 + targetSearchParameter.getParamType().name()); 388 } 389 390 addSortCustomJoin(resourceLinkPredicateBuilder.getColumnTargetResourceId(), chainedPredicateBuilder, null); 391 Condition predicate = chainedPredicateBuilder.createHashIdentityPredicate(targetType, theChain); 392 mySqlBuilder.addPredicate(predicate); 393 394 for (DbColumn next : sortColumn) { 395 mySqlBuilder.addSortNumeric(next, theAscending, myUseAggregate); 396 } 397 } 398 399 public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) { 400 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 401 402 StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder(); 403 Condition hashIdentityPredicate = 404 stringPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 405 406 addSortCustomJoin(firstPredicateBuilder, stringPredicateBuilder, hashIdentityPredicate); 407 408 mySqlBuilder.addSortString(stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate); 409 } 410 411 public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) { 412 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 413 414 TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder(); 415 Condition hashIdentityPredicate = 416 tokenPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 417 418 addSortCustomJoin(firstPredicateBuilder, tokenPredicateBuilder, hashIdentityPredicate); 419 420 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate); 421 mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 422 } 423 424 public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) { 425 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 426 427 UriPredicateBuilder uriPredicateBuilder = mySqlBuilder.createUriPredicateBuilder(); 428 Condition hashIdentityPredicate = 429 uriPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 430 431 addSortCustomJoin(firstPredicateBuilder, uriPredicateBuilder, hashIdentityPredicate); 432 433 mySqlBuilder.addSortString(uriPredicateBuilder.getColumnValue(), theAscending, myUseAggregate); 434 } 435 436 private void addSortCustomJoin( 437 BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder, 438 BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, 439 Condition theCondition) { 440 addSortCustomJoin( 441 theFromJoiningPredicateBuilder.getResourceIdColumn(), theToJoiningPredicateBuilder, theCondition); 442 } 443 444 private void addSortCustomJoin( 445 DbColumn theFromDbColumn, 446 BaseJoiningPredicateBuilder theToJoiningPredicateBuilder, 447 Condition theCondition) { 448 ComboCondition onCondition = 449 mySqlBuilder.createOnCondition(theFromDbColumn, theToJoiningPredicateBuilder.getResourceIdColumn()); 450 451 if (theCondition != null) { 452 onCondition.addCondition(theCondition); 453 } 454 455 mySqlBuilder.addCustomJoin( 456 SelectQuery.JoinType.LEFT_OUTER, 457 theFromDbColumn.getTable(), 458 theToJoiningPredicateBuilder.getTable(), 459 onCondition); 460 } 461 462 public void setUseAggregate(boolean theUseAggregate) { 463 myUseAggregate = theUseAggregate; 464 } 465 466 @SuppressWarnings("unchecked") 467 private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder( 468 PredicateBuilderTypeEnum theType, 469 DbColumn theSourceJoinColumn, 470 String theParamName, 471 Supplier<T> theFactoryMethod) { 472 boolean cacheHit = false; 473 BaseJoiningPredicateBuilder retVal; 474 if (myReusePredicateBuilderTypes.contains(theType)) { 475 PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName); 476 if (myJoinMap == null) { 477 myJoinMap = new HashMap<>(); 478 } 479 retVal = myJoinMap.get(key); 480 if (retVal != null) { 481 cacheHit = true; 482 } else { 483 retVal = theFactoryMethod.get(); 484 myJoinMap.put(key, retVal); 485 } 486 } else { 487 retVal = theFactoryMethod.get(); 488 } 489 490 if (theType == PredicateBuilderTypeEnum.COORDS) { 491 if (myParamNameToPredicateBuilderMap == null) { 492 myParamNameToPredicateBuilderMap = new HashMap<>(); 493 } 494 myParamNameToPredicateBuilderMap.put(theParamName, retVal); 495 } 496 497 return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal); 498 } 499 500 private Condition createPredicateComposite( 501 @Nullable DbColumn theSourceJoinColumn, 502 String theResourceName, 503 String theSpnamePrefix, 504 RuntimeSearchParam theParamDef, 505 List<? extends IQueryParameterType> theNextAnd, 506 RequestPartitionId theRequestPartitionId) { 507 return createPredicateComposite( 508 theSourceJoinColumn, 509 theResourceName, 510 theSpnamePrefix, 511 theParamDef, 512 theNextAnd, 513 theRequestPartitionId, 514 mySqlBuilder); 515 } 516 517 private Condition createPredicateComposite( 518 @Nullable DbColumn theSourceJoinColumn, 519 String theResourceName, 520 String theSpnamePrefix, 521 RuntimeSearchParam theParamDef, 522 List<? extends IQueryParameterType> theNextAnd, 523 RequestPartitionId theRequestPartitionId, 524 SearchQueryBuilder theSqlBuilder) { 525 526 Condition orCondidtion = null; 527 for (IQueryParameterType next : theNextAnd) { 528 529 if (!(next instanceof CompositeParam<?, ?>)) { 530 throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be " 531 + CompositeParam.class.getSimpleName() + ": " + next.getClass()); 532 } 533 CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next; 534 535 List<RuntimeSearchParam> componentParams = 536 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef); 537 RuntimeSearchParam left = componentParams.get(0); 538 IQueryParameterType leftValue = cp.getLeftValue(); 539 Condition leftPredicate = createPredicateCompositePart( 540 theSourceJoinColumn, 541 theResourceName, 542 theSpnamePrefix, 543 left, 544 leftValue, 545 theRequestPartitionId, 546 theSqlBuilder); 547 548 RuntimeSearchParam right = componentParams.get(1); 549 IQueryParameterType rightValue = cp.getRightValue(); 550 Condition rightPredicate = createPredicateCompositePart( 551 theSourceJoinColumn, 552 theResourceName, 553 theSpnamePrefix, 554 right, 555 rightValue, 556 theRequestPartitionId, 557 theSqlBuilder); 558 559 Condition andCondition = toAndPredicate(leftPredicate, rightPredicate); 560 561 if (orCondidtion == null) { 562 orCondidtion = toOrPredicate(andCondition); 563 } else { 564 orCondidtion = toOrPredicate(orCondidtion, andCondition); 565 } 566 } 567 568 return orCondidtion; 569 } 570 571 private Condition createPredicateCompositePart( 572 @Nullable DbColumn theSourceJoinColumn, 573 String theResourceName, 574 String theSpnamePrefix, 575 RuntimeSearchParam theParam, 576 IQueryParameterType theParamValue, 577 RequestPartitionId theRequestPartitionId, 578 SearchQueryBuilder theSqlBuilder) { 579 580 switch (theParam.getParamType()) { 581 case STRING: { 582 return createPredicateString( 583 theSourceJoinColumn, 584 theResourceName, 585 theSpnamePrefix, 586 theParam, 587 Collections.singletonList(theParamValue), 588 null, 589 theRequestPartitionId, 590 theSqlBuilder); 591 } 592 case TOKEN: { 593 return createPredicateToken( 594 theSourceJoinColumn, 595 theResourceName, 596 theSpnamePrefix, 597 theParam, 598 Collections.singletonList(theParamValue), 599 null, 600 theRequestPartitionId, 601 theSqlBuilder); 602 } 603 case DATE: { 604 return createPredicateDate( 605 theSourceJoinColumn, 606 theResourceName, 607 theSpnamePrefix, 608 theParam, 609 Collections.singletonList(theParamValue), 610 toOperation(((DateParam) theParamValue).getPrefix()), 611 theRequestPartitionId, 612 theSqlBuilder); 613 } 614 case QUANTITY: { 615 return createPredicateQuantity( 616 theSourceJoinColumn, 617 theResourceName, 618 theSpnamePrefix, 619 theParam, 620 Collections.singletonList(theParamValue), 621 null, 622 theRequestPartitionId, 623 theSqlBuilder); 624 } 625 case NUMBER: 626 case REFERENCE: 627 case COMPOSITE: 628 case URI: 629 case HAS: 630 case SPECIAL: 631 default: 632 throw new InvalidRequestException(Msg.code(1204) 633 + "Don't know how to handle composite parameter with type of " + theParam.getParamType()); 634 } 635 } 636 637 private Condition createMissingParameterQuery(MissingParameterQueryParams theParams) { 638 if (theParams.getParamType() == RestSearchParameterTypeEnum.COMPOSITE) { 639 ourLog.error("Cannot create missing parameter query for a composite parameter."); 640 return null; 641 } else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 642 if (isEligibleForEmbeddedChainedResourceSearch( 643 theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes()) 644 .supportsUplifted()) { 645 ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search."); 646 return null; 647 } 648 } 649 650 // TODO - Change this when we have HFJ_SPIDX_MISSING table 651 /** 652 * How we search depends on if the 653 * {@link JpaStorageSettings#getIndexMissingFields()} property 654 * is Enabled or Disabled. 655 * 656 * If it is, we will use the SP_MISSING values set into the various 657 * SP_INDX_X tables and search on those ("old" search). 658 * 659 * If it is not set, however, we will try and construct a query that 660 * looks for missing SearchParameters in the SP_IDX_* tables ("new" search). 661 * 662 * You cannot mix and match, however (SP_MISSING is not in HASH_IDENTITY information). 663 * So setting (or unsetting) the IndexMissingFields 664 * property should always be followed up with a /$reindex call. 665 * 666 * --- 667 * 668 * Current limitations: 669 * Checking if a row exists ("new" search) for a given missing field in an SP_INDX_* table 670 * (ie, :missing=true) is slow when there are many resources in the table. (Defaults to 671 * a table scan, since HASH_IDENTITY isn't part of the index). 672 * 673 * However, the "old" search method was slow for the reverse: when looking for resources 674 * that do not have a missing field (:missing=false) for much the same reason. 675 */ 676 SearchQueryBuilder sqlBuilder = theParams.getSqlBuilder(); 677 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.DISABLED) { 678 // new search 679 return createMissingPredicateForUnindexedMissingFields(theParams, sqlBuilder); 680 } else { 681 // old search 682 return createMissingPredicateForIndexedMissingFields(theParams, sqlBuilder); 683 } 684 } 685 686 /** 687 * Old way of searching. 688 * Missing values must be indexed! 689 */ 690 private Condition createMissingPredicateForIndexedMissingFields( 691 MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) { 692 PredicateBuilderTypeEnum predicateType = null; 693 Supplier<? extends BaseJoiningPredicateBuilder> supplier = null; 694 switch (theParams.getParamType()) { 695 case STRING: 696 predicateType = PredicateBuilderTypeEnum.STRING; 697 supplier = () -> sqlBuilder.addStringPredicateBuilder(theParams.getSourceJoinColumn()); 698 break; 699 case NUMBER: 700 predicateType = PredicateBuilderTypeEnum.NUMBER; 701 supplier = () -> sqlBuilder.addNumberPredicateBuilder(theParams.getSourceJoinColumn()); 702 break; 703 case DATE: 704 predicateType = PredicateBuilderTypeEnum.DATE; 705 supplier = () -> sqlBuilder.addDatePredicateBuilder(theParams.getSourceJoinColumn()); 706 break; 707 case TOKEN: 708 predicateType = PredicateBuilderTypeEnum.TOKEN; 709 supplier = () -> sqlBuilder.addTokenPredicateBuilder(theParams.getSourceJoinColumn()); 710 break; 711 case QUANTITY: 712 predicateType = PredicateBuilderTypeEnum.QUANTITY; 713 supplier = () -> sqlBuilder.addQuantityPredicateBuilder(theParams.getSourceJoinColumn()); 714 break; 715 case REFERENCE: 716 case URI: 717 // we expect these values, but the pattern is slightly different; 718 // see below 719 break; 720 case HAS: 721 case SPECIAL: 722 predicateType = PredicateBuilderTypeEnum.COORDS; 723 supplier = () -> sqlBuilder.addCoordsPredicateBuilder(theParams.getSourceJoinColumn()); 724 break; 725 case COMPOSITE: 726 default: 727 break; 728 } 729 730 if (supplier != null) { 731 BaseSearchParamPredicateBuilder join = (BaseSearchParamPredicateBuilder) createOrReusePredicateBuilder( 732 predicateType, theParams.getSourceJoinColumn(), theParams.getParamName(), supplier) 733 .getResult(); 734 735 return join.createPredicateParamMissingForNonReference( 736 theParams.getResourceType(), 737 theParams.getParamName(), 738 theParams.isMissing(), 739 theParams.getRequestPartitionId()); 740 } else { 741 if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 742 SearchParamPresentPredicateBuilder join = 743 sqlBuilder.addSearchParamPresentPredicateBuilder(theParams.getSourceJoinColumn()); 744 return join.createPredicateParamMissingForReference( 745 theParams.getResourceType(), 746 theParams.getParamName(), 747 theParams.isMissing(), 748 theParams.getRequestPartitionId()); 749 } else if (theParams.getParamType() == RestSearchParameterTypeEnum.URI) { 750 UriPredicateBuilder join = sqlBuilder.addUriPredicateBuilder(theParams.getSourceJoinColumn()); 751 return join.createPredicateParamMissingForNonReference( 752 theParams.getResourceType(), 753 theParams.getParamName(), 754 theParams.isMissing(), 755 theParams.getRequestPartitionId()); 756 } else { 757 // we don't expect to see this 758 ourLog.error("Invalid param type " + theParams.getParamType().name()); 759 return null; 760 } 761 } 762 } 763 764 /** 765 * New way of searching for missing fields. 766 * Missing values must not indexed! 767 */ 768 private Condition createMissingPredicateForUnindexedMissingFields( 769 MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) { 770 ResourceTablePredicateBuilder table = sqlBuilder.getOrCreateResourceTablePredicateBuilder(); 771 772 ICanMakeMissingParamPredicate innerQuery = PredicateBuilderFactory.createPredicateBuilderForParamType( 773 theParams.getParamType(), theParams.getSqlBuilder(), this); 774 775 return innerQuery.createPredicateParamMissingValue(new MissingQueryParameterPredicateParams( 776 table, theParams.isMissing(), theParams.getParamName(), theParams.getRequestPartitionId())); 777 } 778 779 public Condition createPredicateCoords( 780 @Nullable DbColumn theSourceJoinColumn, 781 String theResourceName, 782 String theSpnamePrefix, 783 RuntimeSearchParam theSearchParam, 784 List<? extends IQueryParameterType> theList, 785 RequestPartitionId theRequestPartitionId, 786 SearchQueryBuilder theSqlBuilder) { 787 Boolean isMissing = theList.get(0).getMissing(); 788 if (isMissing != null) { 789 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 790 791 return createMissingParameterQuery(new MissingParameterQueryParams( 792 theSqlBuilder, 793 theSearchParam.getParamType(), 794 theList, 795 paramName, 796 theResourceName, 797 theSourceJoinColumn, 798 theRequestPartitionId)); 799 } else { 800 CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder( 801 PredicateBuilderTypeEnum.COORDS, 802 theSourceJoinColumn, 803 theSearchParam.getName(), 804 () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)) 805 .getResult(); 806 807 List<Condition> codePredicates = new ArrayList<>(); 808 for (IQueryParameterType nextOr : theList) { 809 Condition singleCode = predicateBuilder.createPredicateCoords( 810 mySearchParameters, 811 nextOr, 812 theResourceName, 813 theSearchParam, 814 predicateBuilder, 815 theRequestPartitionId); 816 codePredicates.add(singleCode); 817 } 818 819 return predicateBuilder.combineWithRequestPartitionIdPredicate( 820 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 821 } 822 } 823 824 public Condition createPredicateDate( 825 @Nullable DbColumn theSourceJoinColumn, 826 String theResourceName, 827 String theSpnamePrefix, 828 RuntimeSearchParam theSearchParam, 829 List<? extends IQueryParameterType> theList, 830 SearchFilterParser.CompareOperation theOperation, 831 RequestPartitionId theRequestPartitionId) { 832 return createPredicateDate( 833 theSourceJoinColumn, 834 theResourceName, 835 theSpnamePrefix, 836 theSearchParam, 837 theList, 838 theOperation, 839 theRequestPartitionId, 840 mySqlBuilder); 841 } 842 843 public Condition createPredicateDate( 844 @Nullable DbColumn theSourceJoinColumn, 845 String theResourceName, 846 String theSpnamePrefix, 847 RuntimeSearchParam theSearchParam, 848 List<? extends IQueryParameterType> theList, 849 SearchFilterParser.CompareOperation theOperation, 850 RequestPartitionId theRequestPartitionId, 851 SearchQueryBuilder theSqlBuilder) { 852 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 853 854 Boolean isMissing = theList.get(0).getMissing(); 855 if (isMissing != null) { 856 return createMissingParameterQuery(new MissingParameterQueryParams( 857 theSqlBuilder, 858 theSearchParam.getParamType(), 859 theList, 860 paramName, 861 theResourceName, 862 theSourceJoinColumn, 863 theRequestPartitionId)); 864 } else { 865 PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = 866 createOrReusePredicateBuilder( 867 PredicateBuilderTypeEnum.DATE, 868 theSourceJoinColumn, 869 paramName, 870 () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); 871 DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); 872 boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); 873 874 List<Condition> codePredicates = new ArrayList<>(); 875 876 for (IQueryParameterType nextOr : theList) { 877 Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation); 878 codePredicates.add(p); 879 } 880 881 Condition predicate = toOrPredicate(codePredicates); 882 883 if (!cacheHit) { 884 predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate); 885 predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 886 } 887 888 return predicate; 889 } 890 } 891 892 private Condition createPredicateFilter( 893 QueryStack theQueryStack3, 894 SearchFilterParser.BaseFilter theFilter, 895 String theResourceName, 896 RequestDetails theRequest, 897 RequestPartitionId theRequestPartitionId) { 898 899 if (theFilter instanceof SearchFilterParser.FilterParameter) { 900 return createPredicateFilter( 901 theQueryStack3, 902 (SearchFilterParser.FilterParameter) theFilter, 903 theResourceName, 904 theRequest, 905 theRequestPartitionId); 906 } else if (theFilter instanceof SearchFilterParser.FilterLogical) { 907 // Left side 908 Condition xPredicate = createPredicateFilter( 909 theQueryStack3, 910 ((SearchFilterParser.FilterLogical) theFilter).getFilter1(), 911 theResourceName, 912 theRequest, 913 theRequestPartitionId); 914 915 // Right side 916 Condition yPredicate = createPredicateFilter( 917 theQueryStack3, 918 ((SearchFilterParser.FilterLogical) theFilter).getFilter2(), 919 theResourceName, 920 theRequest, 921 theRequestPartitionId); 922 923 if (((SearchFilterParser.FilterLogical) theFilter).getOperation() 924 == SearchFilterParser.FilterLogicalOperation.and) { 925 return ComboCondition.and(xPredicate, yPredicate); 926 } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() 927 == SearchFilterParser.FilterLogicalOperation.or) { 928 return ComboCondition.or(xPredicate, yPredicate); 929 } else { 930 // Shouldn't happen 931 throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation " 932 + ((SearchFilterParser.FilterLogical) theFilter).getOperation()); 933 } 934 } else { 935 return createPredicateFilter( 936 theQueryStack3, 937 ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), 938 theResourceName, 939 theRequest, 940 theRequestPartitionId); 941 } 942 } 943 944 private Condition createPredicateFilter( 945 QueryStack theQueryStack3, 946 SearchFilterParser.FilterParameter theFilter, 947 String theResourceName, 948 RequestDetails theRequest, 949 RequestPartitionId theRequestPartitionId) { 950 951 String paramName = theFilter.getParamPath().getName(); 952 953 switch (paramName) { 954 case IAnyResource.SP_RES_ID: { 955 TokenParam param = new TokenParam(); 956 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 957 return theQueryStack3.createPredicateResourceId( 958 null, 959 Collections.singletonList(Collections.singletonList(param)), 960 theResourceName, 961 theFilter.getOperation(), 962 theRequestPartitionId); 963 } 964 case Constants.PARAM_SOURCE: { 965 TokenParam param = new TokenParam(); 966 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 967 return createPredicateSource(null, Collections.singletonList(param)); 968 } 969 default: 970 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramName); 971 if (searchParam == null) { 972 Collection<String> validNames = 973 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName); 974 String msg = myFhirContext 975 .getLocalizer() 976 .getMessageSanitized( 977 BaseStorageDao.class, 978 "invalidSearchParameter", 979 paramName, 980 theResourceName, 981 validNames); 982 throw new InvalidRequestException(Msg.code(1206) + msg); 983 } 984 RestSearchParameterTypeEnum typeEnum = searchParam.getParamType(); 985 if (typeEnum == RestSearchParameterTypeEnum.URI) { 986 return theQueryStack3.createPredicateUri( 987 null, 988 theResourceName, 989 null, 990 searchParam, 991 Collections.singletonList(new UriParam(theFilter.getValue())), 992 theFilter.getOperation(), 993 theRequest, 994 theRequestPartitionId); 995 } else if (typeEnum == RestSearchParameterTypeEnum.STRING) { 996 return theQueryStack3.createPredicateString( 997 null, 998 theResourceName, 999 null, 1000 searchParam, 1001 Collections.singletonList(new StringParam(theFilter.getValue())), 1002 theFilter.getOperation(), 1003 theRequestPartitionId); 1004 } else if (typeEnum == RestSearchParameterTypeEnum.DATE) { 1005 return theQueryStack3.createPredicateDate( 1006 null, 1007 theResourceName, 1008 null, 1009 searchParam, 1010 Collections.singletonList( 1011 new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), 1012 theFilter.getOperation(), 1013 theRequestPartitionId); 1014 } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) { 1015 return theQueryStack3.createPredicateNumber( 1016 null, 1017 theResourceName, 1018 null, 1019 searchParam, 1020 Collections.singletonList(new NumberParam(theFilter.getValue())), 1021 theFilter.getOperation(), 1022 theRequestPartitionId); 1023 } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) { 1024 SearchFilterParser.CompareOperation operation = theFilter.getOperation(); 1025 String resourceType = 1026 null; // The value can either have (Patient/123) or not have (123) a resource type, either 1027 // way it's not needed here 1028 String chain = (theFilter.getParamPath().getNext() != null) 1029 ? theFilter.getParamPath().getNext().toString() 1030 : null; 1031 String value = theFilter.getValue(); 1032 ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value); 1033 return theQueryStack3.createPredicateReference( 1034 null, 1035 theResourceName, 1036 paramName, 1037 new ArrayList<>(), 1038 Collections.singletonList(referenceParam), 1039 operation, 1040 theRequest, 1041 theRequestPartitionId); 1042 } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) { 1043 return theQueryStack3.createPredicateQuantity( 1044 null, 1045 theResourceName, 1046 null, 1047 searchParam, 1048 Collections.singletonList(new QuantityParam(theFilter.getValue())), 1049 theFilter.getOperation(), 1050 theRequestPartitionId); 1051 } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) { 1052 throw new InvalidRequestException(Msg.code(1207) 1053 + "Composite search parameters not currently supported with _filter clauses"); 1054 } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) { 1055 TokenParam param = new TokenParam(); 1056 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 1057 return theQueryStack3.createPredicateToken( 1058 null, 1059 theResourceName, 1060 null, 1061 searchParam, 1062 Collections.singletonList(param), 1063 theFilter.getOperation(), 1064 theRequestPartitionId); 1065 } 1066 break; 1067 } 1068 return null; 1069 } 1070 1071 private Condition createPredicateHas( 1072 @Nullable DbColumn theSourceJoinColumn, 1073 String theResourceType, 1074 List<List<IQueryParameterType>> theHasParameters, 1075 RequestDetails theRequest, 1076 RequestPartitionId theRequestPartitionId) { 1077 1078 List<Condition> andPredicates = new ArrayList<>(); 1079 for (List<? extends IQueryParameterType> nextOrList : theHasParameters) { 1080 1081 String targetResourceType = null; 1082 String paramReference = null; 1083 String parameterName = null; 1084 1085 String paramName = null; 1086 List<QualifiedParamList> parameters = new ArrayList<>(); 1087 for (IQueryParameterType nextParam : nextOrList) { 1088 HasParam next = (HasParam) nextParam; 1089 targetResourceType = next.getTargetResourceType(); 1090 paramReference = next.getReferenceFieldName(); 1091 parameterName = next.getParameterName(); 1092 paramName = parameterName.replaceAll("\\..*", ""); 1093 parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext))); 1094 } 1095 1096 if (paramName == null) { 1097 continue; 1098 } 1099 1100 try { 1101 myFhirContext.getResourceDefinition(targetResourceType); 1102 } catch (DataFormatException e) { 1103 throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType); 1104 } 1105 1106 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 1107 1108 if (paramName.startsWith("_has:")) { 1109 1110 ourLog.trace("Handing double _has query: {}", paramName); 1111 1112 String qualifier = paramName.substring(4); 1113 for (IQueryParameterType next : nextOrList) { 1114 HasParam nextHasParam = new HasParam(); 1115 nextHasParam.setValueAsQueryToken( 1116 myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext)); 1117 orValues.add(nextHasParam); 1118 } 1119 1120 } else if (paramName.equals(PARAM_ID)) { 1121 1122 for (IQueryParameterType next : nextOrList) { 1123 orValues.add(new TokenParam(next.getValueAsQueryToken(myFhirContext))); 1124 } 1125 1126 } else { 1127 1128 // Ensure that the name of the search param 1129 // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val) 1130 // exists on the target resource type. 1131 RuntimeSearchParam owningParameterDef = 1132 mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramName); 1133 1134 // Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in 1135 // Patient?_has:Observation:subject:code=sys|val) 1136 // exists on the target resource, or in the top-level Resource resource. 1137 mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramReference); 1138 1139 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 1140 mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters); 1141 1142 for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) { 1143 orValues.addAll(next.getValuesAsQueryTokens()); 1144 } 1145 } 1146 1147 // Handle internal chain inside the has. 1148 if (parameterName.contains(".")) { 1149 String chainedPartOfParameter = getChainedPart(parameterName); 1150 orValues.stream() 1151 .filter(qp -> qp instanceof ReferenceParam) 1152 .map(qp -> (ReferenceParam) qp) 1153 .forEach(rp -> rp.setChain(getChainedPart(chainedPartOfParameter))); 1154 1155 parameterName = parameterName.substring(0, parameterName.indexOf('.')); 1156 } 1157 1158 int colonIndex = parameterName.indexOf(':'); 1159 if (colonIndex != -1) { 1160 parameterName = parameterName.substring(0, colonIndex); 1161 } 1162 1163 ResourceLinkPredicateBuilder join = 1164 mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn); 1165 Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId); 1166 1167 List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference, new ArrayList<>()); 1168 if (CollectionUtils.isEmpty(paths)) { 1169 throw new InvalidRequestException(Msg.code(2305) + "Reference field does not exist: " + paramReference); 1170 } 1171 Condition typePredicate = BinaryCondition.equalTo( 1172 join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType)); 1173 Condition pathPredicate = 1174 toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); 1175 Condition linkedPredicate = searchForIdsWithAndOr( 1176 join.getColumnSrcResourceId(), 1177 targetResourceType, 1178 parameterName, 1179 Collections.singletonList(orValues), 1180 theRequest, 1181 theRequestPartitionId, 1182 SearchContainedModeEnum.FALSE); 1183 andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); 1184 } 1185 1186 return toAndPredicate(andPredicates); 1187 } 1188 1189 public Condition createPredicateNumber( 1190 @Nullable DbColumn theSourceJoinColumn, 1191 String theResourceName, 1192 String theSpnamePrefix, 1193 RuntimeSearchParam theSearchParam, 1194 List<? extends IQueryParameterType> theList, 1195 SearchFilterParser.CompareOperation theOperation, 1196 RequestPartitionId theRequestPartitionId) { 1197 return createPredicateNumber( 1198 theSourceJoinColumn, 1199 theResourceName, 1200 theSpnamePrefix, 1201 theSearchParam, 1202 theList, 1203 theOperation, 1204 theRequestPartitionId, 1205 mySqlBuilder); 1206 } 1207 1208 public Condition createPredicateNumber( 1209 @Nullable DbColumn theSourceJoinColumn, 1210 String theResourceName, 1211 String theSpnamePrefix, 1212 RuntimeSearchParam theSearchParam, 1213 List<? extends IQueryParameterType> theList, 1214 SearchFilterParser.CompareOperation theOperation, 1215 RequestPartitionId theRequestPartitionId, 1216 SearchQueryBuilder theSqlBuilder) { 1217 1218 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1219 1220 Boolean isMissing = theList.get(0).getMissing(); 1221 if (isMissing != null) { 1222 return createMissingParameterQuery(new MissingParameterQueryParams( 1223 theSqlBuilder, 1224 theSearchParam.getParamType(), 1225 theList, 1226 paramName, 1227 theResourceName, 1228 theSourceJoinColumn, 1229 theRequestPartitionId)); 1230 } else { 1231 NumberPredicateBuilder join = createOrReusePredicateBuilder( 1232 PredicateBuilderTypeEnum.NUMBER, 1233 theSourceJoinColumn, 1234 paramName, 1235 () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)) 1236 .getResult(); 1237 1238 List<Condition> codePredicates = new ArrayList<>(); 1239 for (IQueryParameterType nextOr : theList) { 1240 1241 if (nextOr instanceof NumberParam) { 1242 NumberParam param = (NumberParam) nextOr; 1243 1244 BigDecimal value = param.getValue(); 1245 if (value == null) { 1246 continue; 1247 } 1248 1249 SearchFilterParser.CompareOperation operation = theOperation; 1250 if (operation == null) { 1251 operation = toOperation(param.getPrefix()); 1252 } 1253 1254 Condition predicate = join.createPredicateNumeric( 1255 theResourceName, paramName, operation, value, theRequestPartitionId, nextOr); 1256 codePredicates.add(predicate); 1257 1258 } else { 1259 throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass()); 1260 } 1261 } 1262 1263 return join.combineWithRequestPartitionIdPredicate( 1264 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1265 } 1266 } 1267 1268 public Condition createPredicateQuantity( 1269 @Nullable DbColumn theSourceJoinColumn, 1270 String theResourceName, 1271 String theSpnamePrefix, 1272 RuntimeSearchParam theSearchParam, 1273 List<? extends IQueryParameterType> theList, 1274 SearchFilterParser.CompareOperation theOperation, 1275 RequestPartitionId theRequestPartitionId) { 1276 return createPredicateQuantity( 1277 theSourceJoinColumn, 1278 theResourceName, 1279 theSpnamePrefix, 1280 theSearchParam, 1281 theList, 1282 theOperation, 1283 theRequestPartitionId, 1284 mySqlBuilder); 1285 } 1286 1287 public Condition createPredicateQuantity( 1288 @Nullable DbColumn theSourceJoinColumn, 1289 String theResourceName, 1290 String theSpnamePrefix, 1291 RuntimeSearchParam theSearchParam, 1292 List<? extends IQueryParameterType> theList, 1293 SearchFilterParser.CompareOperation theOperation, 1294 RequestPartitionId theRequestPartitionId, 1295 SearchQueryBuilder theSqlBuilder) { 1296 1297 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1298 1299 Boolean isMissing = theList.get(0).getMissing(); 1300 if (isMissing != null) { 1301 return createMissingParameterQuery(new MissingParameterQueryParams( 1302 theSqlBuilder, 1303 theSearchParam.getParamType(), 1304 theList, 1305 paramName, 1306 theResourceName, 1307 theSourceJoinColumn, 1308 theRequestPartitionId)); 1309 } else { 1310 List<QuantityParam> quantityParams = 1311 theList.stream().map(t -> QuantityParam.toQuantityParam(t)).collect(Collectors.toList()); 1312 1313 BaseQuantityPredicateBuilder join = null; 1314 boolean normalizedSearchEnabled = myStorageSettings 1315 .getNormalizedQuantitySearchLevel() 1316 .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED); 1317 if (normalizedSearchEnabled) { 1318 List<QuantityParam> normalizedQuantityParams = quantityParams.stream() 1319 .map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t)) 1320 .filter(t -> t != null) 1321 .collect(Collectors.toList()); 1322 1323 if (normalizedQuantityParams.size() == quantityParams.size()) { 1324 join = createOrReusePredicateBuilder( 1325 PredicateBuilderTypeEnum.QUANTITY, 1326 theSourceJoinColumn, 1327 paramName, 1328 () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)) 1329 .getResult(); 1330 quantityParams = normalizedQuantityParams; 1331 } 1332 } 1333 1334 if (join == null) { 1335 join = createOrReusePredicateBuilder( 1336 PredicateBuilderTypeEnum.QUANTITY, 1337 theSourceJoinColumn, 1338 paramName, 1339 () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)) 1340 .getResult(); 1341 } 1342 1343 List<Condition> codePredicates = new ArrayList<>(); 1344 for (QuantityParam nextOr : quantityParams) { 1345 Condition singleCode = join.createPredicateQuantity( 1346 nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId); 1347 codePredicates.add(singleCode); 1348 } 1349 1350 return join.combineWithRequestPartitionIdPredicate( 1351 theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 1352 } 1353 } 1354 1355 public Condition createPredicateReference( 1356 @Nullable DbColumn theSourceJoinColumn, 1357 String theResourceName, 1358 String theParamName, 1359 List<String> theQualifiers, 1360 List<? extends IQueryParameterType> theList, 1361 SearchFilterParser.CompareOperation theOperation, 1362 RequestDetails theRequest, 1363 RequestPartitionId theRequestPartitionId) { 1364 return createPredicateReference( 1365 theSourceJoinColumn, 1366 theResourceName, 1367 theParamName, 1368 theQualifiers, 1369 theList, 1370 theOperation, 1371 theRequest, 1372 theRequestPartitionId, 1373 mySqlBuilder); 1374 } 1375 1376 public Condition createPredicateReference( 1377 @Nullable DbColumn theSourceJoinColumn, 1378 String theResourceName, 1379 String theParamName, 1380 List<String> theQualifiers, 1381 List<? extends IQueryParameterType> theList, 1382 SearchFilterParser.CompareOperation theOperation, 1383 RequestDetails theRequest, 1384 RequestPartitionId theRequestPartitionId, 1385 SearchQueryBuilder theSqlBuilder) { 1386 1387 if ((theOperation != null) 1388 && (theOperation != SearchFilterParser.CompareOperation.eq) 1389 && (theOperation != SearchFilterParser.CompareOperation.ne)) { 1390 throw new InvalidRequestException( 1391 Msg.code(1212) 1392 + "Invalid operator specified for reference predicate. Supported operators for reference predicate are \"eq\" and \"ne\"."); 1393 } 1394 1395 Boolean isMissing = theList.get(0).getMissing(); 1396 if (isMissing != null) { 1397 return createMissingParameterQuery(new MissingParameterQueryParams( 1398 theSqlBuilder, 1399 RestSearchParameterTypeEnum.REFERENCE, 1400 theList, 1401 theParamName, 1402 theResourceName, 1403 theSourceJoinColumn, 1404 theRequestPartitionId)); 1405 } else { 1406 ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder( 1407 PredicateBuilderTypeEnum.REFERENCE, 1408 theSourceJoinColumn, 1409 theParamName, 1410 () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)) 1411 .getResult(); 1412 return predicateBuilder.createPredicate( 1413 theRequest, 1414 theResourceName, 1415 theParamName, 1416 theQualifiers, 1417 theList, 1418 theOperation, 1419 theRequestPartitionId); 1420 } 1421 } 1422 1423 public void addGrouping() { 1424 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1425 mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn()); 1426 } 1427 1428 public Condition createPredicateReferenceForEmbeddedChainedSearchResource( 1429 @Nullable DbColumn theSourceJoinColumn, 1430 String theResourceName, 1431 RuntimeSearchParam theSearchParam, 1432 List<? extends IQueryParameterType> theList, 1433 SearchFilterParser.CompareOperation theOperation, 1434 RequestDetails theRequest, 1435 RequestPartitionId theRequestPartitionId, 1436 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1437 1438 boolean wantChainedAndNormal = 1439 theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 1440 1441 // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse 1442 // builders across different subselects 1443 EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = 1444 EnumSet.copyOf(myReusePredicateBuilderTypes); 1445 if (wantChainedAndNormal) { 1446 myReusePredicateBuilderTypes.clear(); 1447 } 1448 1449 ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); 1450 chainExtractor.deriveChains(theResourceName, theSearchParam, theList); 1451 Map<List<ChainElement>, Set<LeafNodeDefinition>> chains = chainExtractor.getChains(); 1452 1453 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap(); 1454 for (List<ChainElement> nextChain : chains.keySet()) { 1455 Set<LeafNodeDefinition> leafNodes = chains.get(nextChain); 1456 1457 collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum); 1458 } 1459 1460 UnionQuery union = null; 1461 List<Condition> predicates = null; 1462 if (wantChainedAndNormal) { 1463 union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); 1464 } else { 1465 predicates = new ArrayList<>(); 1466 } 1467 1468 predicates = new ArrayList<>(); 1469 for (List<String> nextReferenceLink : referenceLinks.keySet()) { 1470 for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { 1471 SearchQueryBuilder builder; 1472 if (wantChainedAndNormal) { 1473 builder = mySqlBuilder.newChildSqlBuilder(); 1474 } else { 1475 builder = mySqlBuilder; 1476 } 1477 1478 DbColumn previousJoinColumn = null; 1479 1480 // Create a reference link predicates to the subselect for every link but the last one 1481 for (String nextLink : nextReferenceLink) { 1482 // We don't want to call createPredicateReference() here, because the whole point is to avoid the 1483 // recursion. 1484 // TODO: Are we missing any important business logic from that method? All tests are passing. 1485 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = 1486 builder.addReferencePredicateBuilder(this, previousJoinColumn); 1487 builder.addPredicate( 1488 resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink))); 1489 previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId(); 1490 } 1491 1492 Condition containedCondition = createIndexPredicate( 1493 previousJoinColumn, 1494 leafNodeDefinition.getLeafTarget(), 1495 leafNodeDefinition.getLeafPathPrefix(), 1496 leafNodeDefinition.getLeafParamName(), 1497 leafNodeDefinition.getParamDefinition(), 1498 leafNodeDefinition.getOrValues(), 1499 theOperation, 1500 leafNodeDefinition.getQualifiers(), 1501 theRequest, 1502 theRequestPartitionId, 1503 builder); 1504 1505 if (wantChainedAndNormal) { 1506 builder.addPredicate(containedCondition); 1507 union.addQueries(builder.getSelect()); 1508 } else { 1509 predicates.add(containedCondition); 1510 } 1511 } 1512 } 1513 1514 Condition retVal; 1515 if (wantChainedAndNormal) { 1516 1517 if (theSourceJoinColumn == null) { 1518 retVal = new InCondition( 1519 mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); 1520 } else { 1521 // -- for the resource link, need join with target_resource_id 1522 retVal = new InCondition(theSourceJoinColumn, union); 1523 } 1524 1525 } else { 1526 1527 retVal = toOrPredicate(predicates); 1528 } 1529 1530 // restore the state of this collection to turn caching back on before we exit 1531 myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); 1532 return retVal; 1533 } 1534 1535 private void collateChainedSearchOptions( 1536 Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, 1537 List<ChainElement> nextChain, 1538 Set<LeafNodeDefinition> leafNodes, 1539 EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) { 1540 // Manually collapse the chain using all possible variants of contained resource patterns. 1541 // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this 1542 // someday? 1543 // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper 1544 // support for `_contained` 1545 if (nextChain.size() == 1) { 1546 // discrete -> discrete 1547 if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) { 1548 // If !theWantChainedAndNormal that means we're only processing refchains 1549 // so the discrete -> contained case is the only one that applies 1550 updateMapOfReferenceLinks( 1551 referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); 1552 } 1553 1554 // discrete -> contained 1555 RuntimeSearchParam firstParamDefinition = 1556 leafNodes.iterator().next().getParamDefinition(); 1557 updateMapOfReferenceLinks( 1558 referenceLinks, 1559 Lists.newArrayList(), 1560 leafNodes.stream() 1561 .map(t -> t.withPathPrefix( 1562 nextChain.get(0).getResourceType(), 1563 nextChain.get(0).getSearchParameterName())) 1564 // When we're handling discrete->contained the differences between search 1565 // parameters don't matter. E.g. if we're processing "subject.name=foo" 1566 // the name could be Patient:name or Group:name but it doesn't actually 1567 // matter that these are different since in this case both of these end 1568 // up being an identical search in the string table for "subject.name". 1569 .map(t -> t.withParam(firstParamDefinition)) 1570 .collect(Collectors.toSet())); 1571 } else if (nextChain.size() == 2) { 1572 // discrete -> discrete -> discrete 1573 updateMapOfReferenceLinks( 1574 referenceLinks, 1575 Lists.newArrayList( 1576 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1577 leafNodes); 1578 // discrete -> discrete -> contained 1579 updateMapOfReferenceLinks( 1580 referenceLinks, 1581 Lists.newArrayList(nextChain.get(0).getPath()), 1582 leafNodes.stream() 1583 .map(t -> t.withPathPrefix( 1584 nextChain.get(1).getResourceType(), 1585 nextChain.get(1).getSearchParameterName())) 1586 .collect(Collectors.toSet())); 1587 // discrete -> contained -> discrete 1588 updateMapOfReferenceLinks( 1589 referenceLinks, 1590 Lists.newArrayList(mergePaths( 1591 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1592 leafNodes); 1593 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1594 // discrete -> contained -> contained 1595 updateMapOfReferenceLinks( 1596 referenceLinks, 1597 Lists.newArrayList(), 1598 leafNodes.stream() 1599 .map(t -> t.withPathPrefix( 1600 nextChain.get(0).getResourceType(), 1601 nextChain.get(0).getSearchParameterName() + "." 1602 + nextChain.get(1).getSearchParameterName())) 1603 .collect(Collectors.toSet())); 1604 } 1605 } else if (nextChain.size() == 3) { 1606 // discrete -> discrete -> discrete -> discrete 1607 updateMapOfReferenceLinks( 1608 referenceLinks, 1609 Lists.newArrayList( 1610 nextChain.get(0).getPath(), 1611 nextChain.get(1).getPath(), 1612 nextChain.get(2).getPath()), 1613 leafNodes); 1614 // discrete -> discrete -> discrete -> contained 1615 updateMapOfReferenceLinks( 1616 referenceLinks, 1617 Lists.newArrayList( 1618 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1619 leafNodes.stream() 1620 .map(t -> t.withPathPrefix( 1621 nextChain.get(2).getResourceType(), 1622 nextChain.get(2).getSearchParameterName())) 1623 .collect(Collectors.toSet())); 1624 // discrete -> discrete -> contained -> discrete 1625 updateMapOfReferenceLinks( 1626 referenceLinks, 1627 Lists.newArrayList( 1628 nextChain.get(0).getPath(), 1629 mergePaths( 1630 nextChain.get(1).getPath(), nextChain.get(2).getPath())), 1631 leafNodes); 1632 // discrete -> contained -> discrete -> discrete 1633 updateMapOfReferenceLinks( 1634 referenceLinks, 1635 Lists.newArrayList( 1636 mergePaths( 1637 nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1638 nextChain.get(2).getPath()), 1639 leafNodes); 1640 // discrete -> contained -> discrete -> contained 1641 updateMapOfReferenceLinks( 1642 referenceLinks, 1643 Lists.newArrayList(mergePaths( 1644 nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1645 leafNodes.stream() 1646 .map(t -> t.withPathPrefix( 1647 nextChain.get(2).getResourceType(), 1648 nextChain.get(2).getSearchParameterName())) 1649 .collect(Collectors.toSet())); 1650 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 1651 // discrete -> contained -> contained -> discrete 1652 updateMapOfReferenceLinks( 1653 referenceLinks, 1654 Lists.newArrayList(mergePaths( 1655 nextChain.get(0).getPath(), 1656 nextChain.get(1).getPath(), 1657 nextChain.get(2).getPath())), 1658 leafNodes); 1659 // discrete -> discrete -> contained -> contained 1660 updateMapOfReferenceLinks( 1661 referenceLinks, 1662 Lists.newArrayList(nextChain.get(0).getPath()), 1663 leafNodes.stream() 1664 .map(t -> t.withPathPrefix( 1665 nextChain.get(1).getResourceType(), 1666 nextChain.get(1).getSearchParameterName() + "." 1667 + nextChain.get(2).getSearchParameterName())) 1668 .collect(Collectors.toSet())); 1669 // discrete -> contained -> contained -> contained 1670 updateMapOfReferenceLinks( 1671 referenceLinks, 1672 Lists.newArrayList(), 1673 leafNodes.stream() 1674 .map(t -> t.withPathPrefix( 1675 nextChain.get(0).getResourceType(), 1676 nextChain.get(0).getSearchParameterName() + "." 1677 + nextChain.get(1).getSearchParameterName() + "." 1678 + nextChain.get(2).getSearchParameterName())) 1679 .collect(Collectors.toSet())); 1680 } 1681 } else { 1682 // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever 1683 // needs this, we should revisit the approach 1684 throw new InvalidRequestException(Msg.code(2011) 1685 + "The search chain is too long. Only chains of up to three references are supported."); 1686 } 1687 } 1688 1689 private void updateMapOfReferenceLinks( 1690 Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, 1691 ArrayList<String> thePath, 1692 Set<LeafNodeDefinition> theLeafNodesToAdd) { 1693 Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.get(thePath); 1694 if (leafNodes == null) { 1695 leafNodes = Sets.newHashSet(); 1696 theReferenceLinksMap.put(thePath, leafNodes); 1697 } 1698 leafNodes.addAll(theLeafNodesToAdd); 1699 } 1700 1701 private String mergePaths(String... paths) { 1702 String result = ""; 1703 for (String nextPath : paths) { 1704 int separatorIndex = nextPath.indexOf('.'); 1705 if (StringUtils.isEmpty(result)) { 1706 result = nextPath; 1707 } else { 1708 result = result + nextPath.substring(separatorIndex); 1709 } 1710 } 1711 return result; 1712 } 1713 1714 private Condition createIndexPredicate( 1715 DbColumn theSourceJoinColumn, 1716 String theResourceName, 1717 String theSpnamePrefix, 1718 String theParamName, 1719 RuntimeSearchParam theParamDefinition, 1720 ArrayList<IQueryParameterType> theOrValues, 1721 SearchFilterParser.CompareOperation theOperation, 1722 List<String> theQualifiers, 1723 RequestDetails theRequest, 1724 RequestPartitionId theRequestPartitionId, 1725 SearchQueryBuilder theSqlBuilder) { 1726 Condition containedCondition; 1727 1728 switch (theParamDefinition.getParamType()) { 1729 case DATE: 1730 containedCondition = createPredicateDate( 1731 theSourceJoinColumn, 1732 theResourceName, 1733 theSpnamePrefix, 1734 theParamDefinition, 1735 theOrValues, 1736 theOperation, 1737 theRequestPartitionId, 1738 theSqlBuilder); 1739 break; 1740 case NUMBER: 1741 containedCondition = createPredicateNumber( 1742 theSourceJoinColumn, 1743 theResourceName, 1744 theSpnamePrefix, 1745 theParamDefinition, 1746 theOrValues, 1747 theOperation, 1748 theRequestPartitionId, 1749 theSqlBuilder); 1750 break; 1751 case QUANTITY: 1752 containedCondition = createPredicateQuantity( 1753 theSourceJoinColumn, 1754 theResourceName, 1755 theSpnamePrefix, 1756 theParamDefinition, 1757 theOrValues, 1758 theOperation, 1759 theRequestPartitionId, 1760 theSqlBuilder); 1761 break; 1762 case STRING: 1763 containedCondition = createPredicateString( 1764 theSourceJoinColumn, 1765 theResourceName, 1766 theSpnamePrefix, 1767 theParamDefinition, 1768 theOrValues, 1769 theOperation, 1770 theRequestPartitionId, 1771 theSqlBuilder); 1772 break; 1773 case TOKEN: 1774 containedCondition = createPredicateToken( 1775 theSourceJoinColumn, 1776 theResourceName, 1777 theSpnamePrefix, 1778 theParamDefinition, 1779 theOrValues, 1780 theOperation, 1781 theRequestPartitionId, 1782 theSqlBuilder); 1783 break; 1784 case COMPOSITE: 1785 containedCondition = createPredicateComposite( 1786 theSourceJoinColumn, 1787 theResourceName, 1788 theSpnamePrefix, 1789 theParamDefinition, 1790 theOrValues, 1791 theRequestPartitionId, 1792 theSqlBuilder); 1793 break; 1794 case URI: 1795 containedCondition = createPredicateUri( 1796 theSourceJoinColumn, 1797 theResourceName, 1798 theSpnamePrefix, 1799 theParamDefinition, 1800 theOrValues, 1801 theOperation, 1802 theRequest, 1803 theRequestPartitionId, 1804 theSqlBuilder); 1805 break; 1806 case REFERENCE: 1807 containedCondition = createPredicateReference( 1808 theSourceJoinColumn, 1809 theResourceName, 1810 isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, 1811 theQualifiers, 1812 theOrValues, 1813 theOperation, 1814 theRequest, 1815 theRequestPartitionId, 1816 theSqlBuilder); 1817 break; 1818 case HAS: 1819 case SPECIAL: 1820 default: 1821 throw new InvalidRequestException( 1822 Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported."); 1823 } 1824 return containedCondition; 1825 } 1826 1827 @Nullable 1828 public Condition createPredicateResourceId( 1829 @Nullable DbColumn theSourceJoinColumn, 1830 List<List<IQueryParameterType>> theValues, 1831 String theResourceName, 1832 SearchFilterParser.CompareOperation theOperation, 1833 RequestPartitionId theRequestPartitionId) { 1834 ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder(); 1835 return builder.createPredicateResourceId( 1836 theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId); 1837 } 1838 1839 private Condition createPredicateSourceForAndList( 1840 @Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 1841 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1842 1843 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 1844 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1845 andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd)); 1846 } 1847 return toAndPredicate(andPredicates); 1848 } 1849 1850 private Condition createPredicateSource( 1851 @Nullable DbColumn theSourceJoinColumn, List<? extends IQueryParameterType> theList) { 1852 if (myStorageSettings.getStoreMetaSourceInformation() 1853 == JpaStorageSettings.StoreMetaSourceInformationEnum.NONE) { 1854 String msg = myFhirContext.getLocalizer().getMessage(QueryStack.class, "sourceParamDisabled"); 1855 throw new InvalidRequestException(Msg.code(1216) + msg); 1856 } 1857 1858 List<Condition> orPredicates = new ArrayList<>(); 1859 1860 // :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results 1861 // if both sourceUri and requestId are not populated for the resource 1862 Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream() 1863 .filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing()) 1864 .findFirst(); 1865 1866 if (isMissingSourceOptional.isPresent()) { 1867 SourcePredicateBuilder join = 1868 getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER); 1869 orPredicates.add(join.createPredicateMissingSourceUri()); 1870 return toOrPredicate(orPredicates); 1871 } 1872 // for all other cases we use "INNER JOIN" to match search parameters 1873 SourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER); 1874 1875 for (IQueryParameterType nextParameter : theList) { 1876 SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext)); 1877 String sourceUri = sourceParameter.getSourceUri(); 1878 String requestId = sourceParameter.getRequestId(); 1879 if (isNotBlank(sourceUri) && isNotBlank(requestId)) { 1880 orPredicates.add(toAndPredicate( 1881 join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId))); 1882 } else if (isNotBlank(sourceUri)) { 1883 orPredicates.add( 1884 join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri)); 1885 } else if (isNotBlank(requestId)) { 1886 orPredicates.add(join.createPredicateRequestId(requestId)); 1887 } 1888 } 1889 1890 return toOrPredicate(orPredicates); 1891 } 1892 1893 private SourcePredicateBuilder getSourcePredicateBuilder( 1894 @Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 1895 return createOrReusePredicateBuilder( 1896 PredicateBuilderTypeEnum.SOURCE, 1897 theSourceJoinColumn, 1898 Constants.PARAM_SOURCE, 1899 () -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn, theJoinType)) 1900 .getResult(); 1901 } 1902 1903 public Condition createPredicateString( 1904 @Nullable DbColumn theSourceJoinColumn, 1905 String theResourceName, 1906 String theSpnamePrefix, 1907 RuntimeSearchParam theSearchParam, 1908 List<? extends IQueryParameterType> theList, 1909 SearchFilterParser.CompareOperation theOperation, 1910 RequestPartitionId theRequestPartitionId) { 1911 return createPredicateString( 1912 theSourceJoinColumn, 1913 theResourceName, 1914 theSpnamePrefix, 1915 theSearchParam, 1916 theList, 1917 theOperation, 1918 theRequestPartitionId, 1919 mySqlBuilder); 1920 } 1921 1922 public Condition createPredicateString( 1923 @Nullable DbColumn theSourceJoinColumn, 1924 String theResourceName, 1925 String theSpnamePrefix, 1926 RuntimeSearchParam theSearchParam, 1927 List<? extends IQueryParameterType> theList, 1928 SearchFilterParser.CompareOperation theOperation, 1929 RequestPartitionId theRequestPartitionId, 1930 SearchQueryBuilder theSqlBuilder) { 1931 Boolean isMissing = theList.get(0).getMissing(); 1932 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1933 1934 if (isMissing != null) { 1935 return createMissingParameterQuery(new MissingParameterQueryParams( 1936 theSqlBuilder, 1937 theSearchParam.getParamType(), 1938 theList, 1939 paramName, 1940 theResourceName, 1941 theSourceJoinColumn, 1942 theRequestPartitionId)); 1943 } 1944 1945 StringPredicateBuilder join = createOrReusePredicateBuilder( 1946 PredicateBuilderTypeEnum.STRING, 1947 theSourceJoinColumn, 1948 paramName, 1949 () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)) 1950 .getResult(); 1951 1952 List<Condition> codePredicates = new ArrayList<>(); 1953 for (IQueryParameterType nextOr : theList) { 1954 Condition singleCode = join.createPredicateString( 1955 nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation); 1956 codePredicates.add(singleCode); 1957 } 1958 1959 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates)); 1960 } 1961 1962 public Condition createPredicateTag( 1963 @Nullable DbColumn theSourceJoinColumn, 1964 List<List<IQueryParameterType>> theList, 1965 String theParamName, 1966 RequestPartitionId theRequestPartitionId) { 1967 TagTypeEnum tagType; 1968 if (Constants.PARAM_TAG.equals(theParamName)) { 1969 tagType = TagTypeEnum.TAG; 1970 } else if (Constants.PARAM_PROFILE.equals(theParamName)) { 1971 tagType = TagTypeEnum.PROFILE; 1972 } else if (Constants.PARAM_SECURITY.equals(theParamName)) { 1973 tagType = TagTypeEnum.SECURITY_LABEL; 1974 } else { 1975 throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen 1976 } 1977 1978 List<Condition> andPredicates = new ArrayList<>(); 1979 for (List<? extends IQueryParameterType> nextAndParams : theList) { 1980 if (!checkHaveTags(nextAndParams, theParamName)) { 1981 continue; 1982 } 1983 1984 List<Triple<String, String, String>> tokens = Lists.newArrayList(); 1985 boolean paramInverted = populateTokens(tokens, nextAndParams); 1986 if (tokens.isEmpty()) { 1987 continue; 1988 } 1989 1990 Condition tagPredicate; 1991 BaseJoiningPredicateBuilder join; 1992 if (paramInverted) { 1993 1994 SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(); 1995 TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null); 1996 sqlBuilder.addPredicate( 1997 tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId)); 1998 SelectQuery sql = sqlBuilder.getSelect(); 1999 2000 join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2001 Expression subSelect = new Subquery(sql); 2002 tagPredicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); 2003 2004 } else { 2005 // Tag table can't be a query root because it will include deleted resources, and can't select by 2006 // resource type 2007 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 2008 2009 TagPredicateBuilder tagJoin = createOrReusePredicateBuilder( 2010 PredicateBuilderTypeEnum.TAG, 2011 theSourceJoinColumn, 2012 theParamName, 2013 () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn)) 2014 .getResult(); 2015 tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId); 2016 join = tagJoin; 2017 } 2018 2019 andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate)); 2020 } 2021 2022 return toAndPredicate(andPredicates); 2023 } 2024 2025 private boolean populateTokens( 2026 List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) { 2027 boolean paramInverted = false; 2028 2029 for (IQueryParameterType nextOrParam : theAndParams) { 2030 String code; 2031 String system; 2032 if (nextOrParam instanceof TokenParam) { 2033 TokenParam nextParam = (TokenParam) nextOrParam; 2034 code = nextParam.getValue(); 2035 system = nextParam.getSystem(); 2036 if (nextParam.getModifier() == TokenParamModifier.NOT) { 2037 paramInverted = true; 2038 } 2039 } else { 2040 UriParam nextParam = (UriParam) nextOrParam; 2041 code = nextParam.getValue(); 2042 system = null; 2043 } 2044 2045 if (isNotBlank(code)) { 2046 theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code)); 2047 } 2048 } 2049 return paramInverted; 2050 } 2051 2052 private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) { 2053 for (IQueryParameterType nextParamUncasted : theParams) { 2054 if (nextParamUncasted instanceof TokenParam) { 2055 TokenParam nextParam = (TokenParam) nextParamUncasted; 2056 if (isNotBlank(nextParam.getValue())) { 2057 return true; 2058 } 2059 if (isNotBlank(nextParam.getSystem())) { 2060 throw new TokenParamFormatInvalidRequestException( 2061 Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext)); 2062 } 2063 } 2064 2065 UriParam nextParam = (UriParam) nextParamUncasted; 2066 if (isNotBlank(nextParam.getValue())) { 2067 return true; 2068 } 2069 } 2070 2071 return false; 2072 } 2073 2074 public Condition createPredicateToken( 2075 @Nullable DbColumn theSourceJoinColumn, 2076 String theResourceName, 2077 String theSpnamePrefix, 2078 RuntimeSearchParam theSearchParam, 2079 List<? extends IQueryParameterType> theList, 2080 SearchFilterParser.CompareOperation theOperation, 2081 RequestPartitionId theRequestPartitionId) { 2082 return createPredicateToken( 2083 theSourceJoinColumn, 2084 theResourceName, 2085 theSpnamePrefix, 2086 theSearchParam, 2087 theList, 2088 theOperation, 2089 theRequestPartitionId, 2090 mySqlBuilder); 2091 } 2092 2093 public Condition createPredicateToken( 2094 @Nullable DbColumn theSourceJoinColumn, 2095 String theResourceName, 2096 String theSpnamePrefix, 2097 RuntimeSearchParam theSearchParam, 2098 List<? extends IQueryParameterType> theList, 2099 SearchFilterParser.CompareOperation theOperation, 2100 RequestPartitionId theRequestPartitionId, 2101 SearchQueryBuilder theSqlBuilder) { 2102 2103 List<IQueryParameterType> tokens = new ArrayList<>(); 2104 2105 boolean paramInverted = false; 2106 TokenParamModifier modifier; 2107 2108 for (IQueryParameterType nextOr : theList) { 2109 if (nextOr instanceof TokenParam) { 2110 if (!((TokenParam) nextOr).isEmpty()) { 2111 TokenParam id = (TokenParam) nextOr; 2112 if (id.isText()) { 2113 2114 // Check whether the :text modifier is actually enabled here 2115 boolean tokenTextIndexingEnabled = 2116 BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam( 2117 myStorageSettings, theSearchParam); 2118 if (!tokenTextIndexingEnabled) { 2119 String msg; 2120 if (myStorageSettings.isSuppressStringIndexingInTokens()) { 2121 msg = myFhirContext 2122 .getLocalizer() 2123 .getMessage(QueryStack.class, "textModifierDisabledForServer"); 2124 } else { 2125 msg = myFhirContext 2126 .getLocalizer() 2127 .getMessage(QueryStack.class, "textModifierDisabledForSearchParam"); 2128 } 2129 throw new MethodNotAllowedException(Msg.code(1219) + msg); 2130 } 2131 return createPredicateString( 2132 theSourceJoinColumn, 2133 theResourceName, 2134 theSpnamePrefix, 2135 theSearchParam, 2136 theList, 2137 null, 2138 theRequestPartitionId, 2139 theSqlBuilder); 2140 } 2141 2142 modifier = id.getModifier(); 2143 // for :not modifier, create a token and remove the :not modifier 2144 if (modifier == TokenParamModifier.NOT) { 2145 tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue())); 2146 paramInverted = true; 2147 } else { 2148 tokens.add(nextOr); 2149 } 2150 } 2151 } else { 2152 tokens.add(nextOr); 2153 } 2154 } 2155 2156 if (tokens.isEmpty()) { 2157 return null; 2158 } 2159 2160 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2161 Condition predicate; 2162 BaseJoiningPredicateBuilder join; 2163 2164 if (paramInverted) { 2165 SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(); 2166 TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); 2167 sqlBuilder.addPredicate(tokenSelector.createPredicateToken( 2168 tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); 2169 SelectQuery sql = sqlBuilder.getSelect(); 2170 Expression subSelect = new Subquery(sql); 2171 2172 join = theSqlBuilder.getOrCreateFirstPredicateBuilder(); 2173 2174 if (theSourceJoinColumn == null) { 2175 predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); 2176 } else { 2177 // -- for the resource link, need join with target_resource_id 2178 predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true); 2179 } 2180 2181 } else { 2182 Boolean isMissing = theList.get(0).getMissing(); 2183 if (isMissing != null) { 2184 return createMissingParameterQuery(new MissingParameterQueryParams( 2185 theSqlBuilder, 2186 theSearchParam.getParamType(), 2187 theList, 2188 paramName, 2189 theResourceName, 2190 theSourceJoinColumn, 2191 theRequestPartitionId)); 2192 } 2193 2194 TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder( 2195 PredicateBuilderTypeEnum.TOKEN, 2196 theSourceJoinColumn, 2197 paramName, 2198 () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)) 2199 .getResult(); 2200 2201 predicate = tokenJoin.createPredicateToken( 2202 tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId); 2203 join = tokenJoin; 2204 } 2205 2206 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2207 } 2208 2209 public Condition createPredicateUri( 2210 @Nullable DbColumn theSourceJoinColumn, 2211 String theResourceName, 2212 String theSpnamePrefix, 2213 RuntimeSearchParam theSearchParam, 2214 List<? extends IQueryParameterType> theList, 2215 SearchFilterParser.CompareOperation theOperation, 2216 RequestDetails theRequestDetails, 2217 RequestPartitionId theRequestPartitionId) { 2218 return createPredicateUri( 2219 theSourceJoinColumn, 2220 theResourceName, 2221 theSpnamePrefix, 2222 theSearchParam, 2223 theList, 2224 theOperation, 2225 theRequestDetails, 2226 theRequestPartitionId, 2227 mySqlBuilder); 2228 } 2229 2230 public Condition createPredicateUri( 2231 @Nullable DbColumn theSourceJoinColumn, 2232 String theResourceName, 2233 String theSpnamePrefix, 2234 RuntimeSearchParam theSearchParam, 2235 List<? extends IQueryParameterType> theList, 2236 SearchFilterParser.CompareOperation theOperation, 2237 RequestDetails theRequestDetails, 2238 RequestPartitionId theRequestPartitionId, 2239 SearchQueryBuilder theSqlBuilder) { 2240 2241 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 2242 2243 Boolean isMissing = theList.get(0).getMissing(); 2244 if (isMissing != null) { 2245 return createMissingParameterQuery(new MissingParameterQueryParams( 2246 theSqlBuilder, 2247 theSearchParam.getParamType(), 2248 theList, 2249 paramName, 2250 theResourceName, 2251 theSourceJoinColumn, 2252 theRequestPartitionId)); 2253 } else { 2254 UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); 2255 2256 Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails); 2257 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 2258 } 2259 } 2260 2261 public QueryStack newChildQueryFactoryWithFullBuilderReuse() { 2262 return new QueryStack( 2263 mySearchParameters, 2264 myStorageSettings, 2265 myFhirContext, 2266 mySqlBuilder, 2267 mySearchParamRegistry, 2268 myPartitionSettings, 2269 EnumSet.allOf(PredicateBuilderTypeEnum.class)); 2270 } 2271 2272 @Nullable 2273 public Condition searchForIdsWithAndOr( 2274 @Nullable DbColumn theSourceJoinColumn, 2275 String theResourceName, 2276 String theParamName, 2277 List<List<IQueryParameterType>> theAndOrParams, 2278 RequestDetails theRequest, 2279 RequestPartitionId theRequestPartitionId, 2280 SearchContainedModeEnum theSearchContainedMode) { 2281 2282 if (theAndOrParams.isEmpty()) { 2283 return null; 2284 } 2285 2286 switch (theParamName) { 2287 case IAnyResource.SP_RES_ID: 2288 return createPredicateResourceId( 2289 theSourceJoinColumn, theAndOrParams, theResourceName, null, theRequestPartitionId); 2290 2291 case PARAM_HAS: 2292 return createPredicateHas( 2293 theSourceJoinColumn, theResourceName, theAndOrParams, theRequest, theRequestPartitionId); 2294 2295 case Constants.PARAM_TAG: 2296 case Constants.PARAM_PROFILE: 2297 case Constants.PARAM_SECURITY: 2298 if (myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE) { 2299 return createPredicateSearchParameter( 2300 theSourceJoinColumn, 2301 theResourceName, 2302 theParamName, 2303 theAndOrParams, 2304 theRequest, 2305 theRequestPartitionId); 2306 } else { 2307 return createPredicateTag(theSourceJoinColumn, theAndOrParams, theParamName, theRequestPartitionId); 2308 } 2309 2310 case Constants.PARAM_SOURCE: 2311 return createPredicateSourceForAndList(theSourceJoinColumn, theAndOrParams); 2312 2313 default: 2314 return createPredicateSearchParameter( 2315 theSourceJoinColumn, 2316 theResourceName, 2317 theParamName, 2318 theAndOrParams, 2319 theRequest, 2320 theRequestPartitionId); 2321 } 2322 } 2323 2324 @Nullable 2325 private Condition createPredicateSearchParameter( 2326 @Nullable DbColumn theSourceJoinColumn, 2327 String theResourceName, 2328 String theParamName, 2329 List<List<IQueryParameterType>> theAndOrParams, 2330 RequestDetails theRequest, 2331 RequestPartitionId theRequestPartitionId) { 2332 List<Condition> andPredicates = new ArrayList<>(); 2333 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); 2334 if (nextParamDef != null) { 2335 2336 if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) { 2337 if (theRequestPartitionId.isAllPartitions()) { 2338 throw new PreconditionFailedException( 2339 Msg.code(1220) + "This server is not configured to support search against all partitions"); 2340 } 2341 } 2342 2343 switch (nextParamDef.getParamType()) { 2344 case DATE: 2345 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2346 // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt' 2347 // to create the predicateDate instead of generic one with operation = null 2348 SearchFilterParser.CompareOperation operation = null; 2349 if (nextAnd.size() > 0) { 2350 DateParam param = (DateParam) nextAnd.get(0); 2351 operation = toOperation(param.getPrefix()); 2352 } 2353 andPredicates.add(createPredicateDate( 2354 theSourceJoinColumn, 2355 theResourceName, 2356 null, 2357 nextParamDef, 2358 nextAnd, 2359 operation, 2360 theRequestPartitionId)); 2361 } 2362 break; 2363 case QUANTITY: 2364 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2365 SearchFilterParser.CompareOperation operation = null; 2366 if (nextAnd.size() > 0) { 2367 QuantityParam param = (QuantityParam) nextAnd.get(0); 2368 operation = toOperation(param.getPrefix()); 2369 } 2370 andPredicates.add(createPredicateQuantity( 2371 theSourceJoinColumn, 2372 theResourceName, 2373 null, 2374 nextParamDef, 2375 nextAnd, 2376 operation, 2377 theRequestPartitionId)); 2378 } 2379 break; 2380 case REFERENCE: 2381 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2382 2383 // Handle Search Parameters where the name is a full chain 2384 // (e.g. SearchParameter with name=composition.patient.identifier) 2385 if (handleFullyChainedParameter( 2386 theSourceJoinColumn, 2387 theResourceName, 2388 theParamName, 2389 theRequest, 2390 theRequestPartitionId, 2391 andPredicates, 2392 nextAnd)) { 2393 break; 2394 } 2395 2396 EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = 2397 isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd); 2398 if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) { 2399 andPredicates.add(createPredicateReference( 2400 theSourceJoinColumn, 2401 theResourceName, 2402 theParamName, 2403 new ArrayList<>(), 2404 nextAnd, 2405 null, 2406 theRequest, 2407 theRequestPartitionId)); 2408 } else { 2409 andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource( 2410 theSourceJoinColumn, 2411 theResourceName, 2412 nextParamDef, 2413 nextAnd, 2414 null, 2415 theRequest, 2416 theRequestPartitionId, 2417 embeddedChainedSearchModeEnum)); 2418 } 2419 } 2420 break; 2421 case STRING: 2422 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2423 andPredicates.add(createPredicateString( 2424 theSourceJoinColumn, 2425 theResourceName, 2426 null, 2427 nextParamDef, 2428 nextAnd, 2429 SearchFilterParser.CompareOperation.sw, 2430 theRequestPartitionId)); 2431 } 2432 break; 2433 case TOKEN: 2434 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2435 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2436 andPredicates.add(createPredicateCoords( 2437 theSourceJoinColumn, 2438 theResourceName, 2439 null, 2440 nextParamDef, 2441 nextAnd, 2442 theRequestPartitionId, 2443 mySqlBuilder)); 2444 } else { 2445 andPredicates.add(createPredicateToken( 2446 theSourceJoinColumn, 2447 theResourceName, 2448 null, 2449 nextParamDef, 2450 nextAnd, 2451 null, 2452 theRequestPartitionId)); 2453 } 2454 } 2455 break; 2456 case NUMBER: 2457 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2458 andPredicates.add(createPredicateNumber( 2459 theSourceJoinColumn, 2460 theResourceName, 2461 null, 2462 nextParamDef, 2463 nextAnd, 2464 null, 2465 theRequestPartitionId)); 2466 } 2467 break; 2468 case COMPOSITE: 2469 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2470 andPredicates.add(createPredicateComposite( 2471 theSourceJoinColumn, 2472 theResourceName, 2473 null, 2474 nextParamDef, 2475 nextAnd, 2476 theRequestPartitionId)); 2477 } 2478 break; 2479 case URI: 2480 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2481 andPredicates.add(createPredicateUri( 2482 theSourceJoinColumn, 2483 theResourceName, 2484 null, 2485 nextParamDef, 2486 nextAnd, 2487 SearchFilterParser.CompareOperation.eq, 2488 theRequest, 2489 theRequestPartitionId)); 2490 } 2491 break; 2492 case HAS: 2493 case SPECIAL: 2494 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 2495 if (LOCATION_POSITION.equals(nextParamDef.getPath())) { 2496 andPredicates.add(createPredicateCoords( 2497 theSourceJoinColumn, 2498 theResourceName, 2499 null, 2500 nextParamDef, 2501 nextAnd, 2502 theRequestPartitionId, 2503 mySqlBuilder)); 2504 } 2505 } 2506 break; 2507 } 2508 } else { 2509 // These are handled later 2510 if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) { 2511 if (Constants.PARAM_FILTER.equals(theParamName)) { 2512 2513 // Parse the predicates enumerated in the _filter separated by AND or OR... 2514 if (theAndOrParams.get(0).get(0) instanceof StringParam) { 2515 String filterString = 2516 ((StringParam) theAndOrParams.get(0).get(0)).getValue(); 2517 SearchFilterParser.BaseFilter filter; 2518 try { 2519 filter = SearchFilterParser.parse(filterString); 2520 } catch (SearchFilterParser.FilterSyntaxException theE) { 2521 throw new InvalidRequestException( 2522 Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage()); 2523 } 2524 if (filter != null) { 2525 2526 if (!myStorageSettings.isFilterParameterEnabled()) { 2527 throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER 2528 + " parameter is disabled on this server"); 2529 } 2530 2531 Condition predicate = createPredicateFilter( 2532 this, filter, theResourceName, theRequest, theRequestPartitionId); 2533 if (predicate != null) { 2534 mySqlBuilder.addPredicate(predicate); 2535 } 2536 } 2537 } 2538 2539 } else { 2540 String msg = myFhirContext 2541 .getLocalizer() 2542 .getMessageSanitized( 2543 BaseStorageDao.class, 2544 "invalidSearchParameter", 2545 theParamName, 2546 theResourceName, 2547 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName)); 2548 throw new InvalidRequestException(Msg.code(1223) + msg); 2549 } 2550 } 2551 } 2552 2553 return toAndPredicate(andPredicates); 2554 } 2555 2556 /** 2557 * This method handles the case of Search Parameters where the name/code 2558 * in the SP is a full chain expression. Normally to handle an expression 2559 * like <code>Observation?subject.name=foo</code> are handled by a SP 2560 * with a type of REFERENCE where the name is "subject". That is not 2561 * handled here. On the other hand, if the SP has a name value containing 2562 * the full chain (e.g. "subject.name") we handle that here. 2563 * 2564 * @return Returns {@literal true} if the search parameter was handled 2565 * by this method 2566 */ 2567 private boolean handleFullyChainedParameter( 2568 @Nullable DbColumn theSourceJoinColumn, 2569 String theResourceName, 2570 String theParamName, 2571 RequestDetails theRequest, 2572 RequestPartitionId theRequestPartitionId, 2573 List<Condition> andPredicates, 2574 List<? extends IQueryParameterType> nextAnd) { 2575 if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) { 2576 ReferenceParam param = (ReferenceParam) nextAnd.get(0); 2577 if (isNotBlank(param.getChain())) { 2578 String fullName = theParamName + "." + param.getChain(); 2579 RuntimeSearchParam fullChainParam = 2580 mySearchParamRegistry.getActiveSearchParam(theResourceName, fullName); 2581 if (fullChainParam != null) { 2582 List<IQueryParameterType> swappedParamTypes = nextAnd.stream() 2583 .map(t -> newParameterInstance(fullChainParam, null, t.getValueAsQueryToken(myFhirContext))) 2584 .collect(Collectors.toList()); 2585 List<List<IQueryParameterType>> params = List.of(swappedParamTypes); 2586 Condition predicate = createPredicateSearchParameter( 2587 theSourceJoinColumn, theResourceName, fullName, params, theRequest, theRequestPartitionId); 2588 andPredicates.add(predicate); 2589 return true; 2590 } 2591 } 2592 } 2593 return false; 2594 } 2595 2596 /** 2597 * When searching using a chained search expression (e.g. "Patient?organization.name=foo") 2598 * we have a few options: 2599 * <ul> 2600 * <li> 2601 * A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for 2602 * paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2603 * with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY} 2604 * which is the standard searching case. Let's guess that 99.9% of all searches work 2605 * this way. 2606 * </ul> 2607 * <li> 2608 * B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString} 2609 * with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}. 2610 * We only do this if there is an uplifted refchain declared on the "organization" 2611 * search parameter for the "name" search parameter, and contained indexing is disabled. 2612 * This kind of index can come from indexing normal references where the search parameter 2613 * has an uplifted refchain declared, and it can also come from indexing contained resources. 2614 * For both of these cases, the actual index in the database is identical. But the important 2615 * difference is that when you're searching for contained resources you also want to 2616 * search for normal references. When you're searching for explicit refchains, no normal 2617 * indexes matter because they'd be a duplicate of the uplifted refchain. 2618 * </li> 2619 * <li> 2620 * C. We can also do both and return a union of the two, using 2621 * {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained 2622 * resource indexing is enabled since we have to assume there may be indexes 2623 * on "organization" for both contained and non-contained Organization. 2624 * resources. 2625 * </li> 2626 */ 2627 private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch( 2628 String theResourceType, String theParameterName, List<? extends IQueryParameterType> theParameter) { 2629 boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); 2630 boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains(); 2631 2632 if (!indexOnContainedResources && !indexOnUpliftedRefchains) { 2633 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2634 } 2635 2636 boolean haveUpliftCandidates = theParameter.stream() 2637 .filter(t -> t instanceof ReferenceParam) 2638 .map(t -> ((ReferenceParam) t).getChain()) 2639 .filter(StringUtils::isNotBlank) 2640 // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we 2641 // ever want to support this, it would be really hard to do. 2642 .filter(t -> !t.startsWith(PARAM_HAS + ":")) 2643 .anyMatch(t -> { 2644 if (indexOnContainedResources) { 2645 return true; 2646 } 2647 RuntimeSearchParam param = 2648 mySearchParamRegistry.getActiveSearchParam(theResourceType, theParameterName); 2649 return param != null && param.hasUpliftRefchain(t); 2650 }); 2651 2652 if (haveUpliftCandidates) { 2653 if (indexOnContainedResources) { 2654 return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN; 2655 } 2656 return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY; 2657 } else { 2658 return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY; 2659 } 2660 } 2661 2662 public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { 2663 ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder(); 2664 Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString); 2665 mySqlBuilder.addPredicate(predicate); 2666 } 2667 2668 public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { 2669 ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder = 2670 mySqlBuilder.addComboNonUniquePredicateBuilder(); 2671 Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString); 2672 mySqlBuilder.addPredicate(predicate); 2673 } 2674 2675 // expand out the pids 2676 public void addPredicateEverythingOperation( 2677 String theResourceName, List<String> theTypeSourceResourceNames, Long... theTargetPids) { 2678 ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); 2679 Condition predicate = 2680 table.createEverythingPredicate(theResourceName, theTypeSourceResourceNames, theTargetPids); 2681 mySqlBuilder.addPredicate(predicate); 2682 } 2683 2684 public IQueryParameterType newParameterInstance( 2685 RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) { 2686 IQueryParameterType qp = newParameterInstance(theParam); 2687 2688 qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken); 2689 return qp; 2690 } 2691 2692 private IQueryParameterType newParameterInstance(RuntimeSearchParam theParam) { 2693 2694 IQueryParameterType qp; 2695 switch (theParam.getParamType()) { 2696 case DATE: 2697 qp = new DateParam(); 2698 break; 2699 case NUMBER: 2700 qp = new NumberParam(); 2701 break; 2702 case QUANTITY: 2703 qp = new QuantityParam(); 2704 break; 2705 case STRING: 2706 qp = new StringParam(); 2707 break; 2708 case TOKEN: 2709 qp = new TokenParam(); 2710 break; 2711 case COMPOSITE: 2712 List<RuntimeSearchParam> compositeOf = 2713 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam); 2714 if (compositeOf.size() != 2) { 2715 throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has " 2716 + compositeOf.size() + " composite parts. Don't know how handlt this."); 2717 } 2718 IQueryParameterType leftParam = newParameterInstance(compositeOf.get(0)); 2719 IQueryParameterType rightParam = newParameterInstance(compositeOf.get(1)); 2720 qp = new CompositeParam<>(leftParam, rightParam); 2721 break; 2722 case URI: 2723 qp = new UriParam(); 2724 break; 2725 case REFERENCE: 2726 qp = new ReferenceParam(); 2727 break; 2728 case SPECIAL: 2729 qp = new SpecialParam(); 2730 break; 2731 case HAS: 2732 default: 2733 throw new InvalidRequestException( 2734 Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); 2735 } 2736 return qp; 2737 } 2738 2739 /** 2740 * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum 2741 */ 2742 enum EmbeddedChainedSearchModeEnum { 2743 UPLIFTED_ONLY(true), 2744 UPLIFTED_AND_REF_JOIN(true), 2745 REF_JOIN_ONLY(false); 2746 2747 private final boolean mySupportsUplifted; 2748 2749 EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) { 2750 mySupportsUplifted = theSupportsUplifted; 2751 } 2752 2753 public boolean supportsUplifted() { 2754 return mySupportsUplifted; 2755 } 2756 } 2757 2758 private static final class ChainElement { 2759 private final String myResourceType; 2760 private final String mySearchParameterName; 2761 private final String myPath; 2762 2763 public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { 2764 this.myResourceType = theResourceType; 2765 this.mySearchParameterName = theSearchParameterName; 2766 this.myPath = thePath; 2767 } 2768 2769 public String getResourceType() { 2770 return myResourceType; 2771 } 2772 2773 public String getPath() { 2774 return myPath; 2775 } 2776 2777 public String getSearchParameterName() { 2778 return mySearchParameterName; 2779 } 2780 2781 @Override 2782 public boolean equals(Object o) { 2783 if (this == o) return true; 2784 if (o == null || getClass() != o.getClass()) return false; 2785 ChainElement that = (ChainElement) o; 2786 return myResourceType.equals(that.myResourceType) 2787 && mySearchParameterName.equals(that.mySearchParameterName) 2788 && myPath.equals(that.myPath); 2789 } 2790 2791 @Override 2792 public int hashCode() { 2793 return Objects.hash(myResourceType, mySearchParameterName, myPath); 2794 } 2795 } 2796 2797 private class ReferenceChainExtractor { 2798 private final Map<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap(); 2799 2800 public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() { 2801 return myChains; 2802 } 2803 2804 private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { 2805 return split(theReferenceParam.getChain(), '.').length <= 3; 2806 } 2807 2808 private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { 2809 List<String> pathsForType = theSearchParam.getPathsSplit().stream() 2810 .map(String::trim) 2811 .filter(t -> t.startsWith(theResourceType)) 2812 .collect(Collectors.toList()); 2813 if (pathsForType.isEmpty()) { 2814 ourLog.warn( 2815 "Search parameter {} does not have a path for resource type {}.", 2816 theSearchParam.getName(), 2817 theResourceType); 2818 } 2819 2820 return pathsForType; 2821 } 2822 2823 public void deriveChains( 2824 String theResourceType, 2825 RuntimeSearchParam theSearchParam, 2826 List<? extends IQueryParameterType> theList) { 2827 List<String> paths = extractPaths(theResourceType, theSearchParam); 2828 for (String path : paths) { 2829 List<ChainElement> searchParams = Lists.newArrayList(); 2830 searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); 2831 for (IQueryParameterType nextOr : theList) { 2832 String targetValue = nextOr.getValueAsQueryToken(myFhirContext); 2833 if (nextOr instanceof ReferenceParam) { 2834 ReferenceParam referenceParam = (ReferenceParam) nextOr; 2835 if (!isReferenceParamValid(referenceParam)) { 2836 throw new InvalidRequestException(Msg.code(2007) + "The search chain " 2837 + theSearchParam.getName() + "." + referenceParam.getChain() 2838 + " is too long. Only chains up to three references are supported."); 2839 } 2840 2841 String targetChain = referenceParam.getChain(); 2842 List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType()); 2843 2844 processNextLinkInChain( 2845 searchParams, 2846 theSearchParam, 2847 targetChain, 2848 targetValue, 2849 qualifiers, 2850 referenceParam.getResourceType()); 2851 } 2852 } 2853 } 2854 } 2855 2856 private void processNextLinkInChain( 2857 List<ChainElement> theSearchParams, 2858 RuntimeSearchParam thePreviousSearchParam, 2859 String theChain, 2860 String theTargetValue, 2861 List<String> theQualifiers, 2862 String theResourceType) { 2863 2864 String nextParamName = theChain; 2865 String nextChain = null; 2866 String nextQualifier = null; 2867 int linkIndex = theChain.indexOf('.'); 2868 if (linkIndex != -1) { 2869 nextParamName = theChain.substring(0, linkIndex); 2870 nextChain = theChain.substring(linkIndex + 1); 2871 } 2872 2873 int qualifierIndex = nextParamName.indexOf(':'); 2874 if (qualifierIndex != -1) { 2875 nextParamName = nextParamName.substring(0, qualifierIndex); 2876 nextQualifier = nextParamName.substring(qualifierIndex); 2877 } 2878 2879 List<String> qualifiersBranch = Lists.newArrayList(); 2880 qualifiersBranch.addAll(theQualifiers); 2881 qualifiersBranch.add(nextQualifier); 2882 2883 boolean searchParamFound = false; 2884 for (String nextTarget : thePreviousSearchParam.getTargets()) { 2885 RuntimeSearchParam nextSearchParam = null; 2886 if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) { 2887 nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName); 2888 } 2889 if (nextSearchParam != null) { 2890 searchParamFound = true; 2891 // If we find a search param on this resource type for this parameter name, keep iterating 2892 // Otherwise, abandon this branch and carry on to the next one 2893 if (StringUtils.isEmpty(nextChain)) { 2894 // We've reached the end of the chain 2895 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 2896 2897 if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { 2898 orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); 2899 } else { 2900 IQueryParameterType qp = newParameterInstance(nextSearchParam); 2901 qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); 2902 orValues.add(qp); 2903 } 2904 2905 Set<LeafNodeDefinition> leafNodes = myChains.get(theSearchParams); 2906 if (leafNodes == null) { 2907 leafNodes = Sets.newHashSet(); 2908 myChains.put(theSearchParams, leafNodes); 2909 } 2910 leafNodes.add(new LeafNodeDefinition( 2911 nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); 2912 } else { 2913 List<String> nextPaths = extractPaths(nextTarget, nextSearchParam); 2914 for (String nextPath : nextPaths) { 2915 List<ChainElement> searchParamBranch = Lists.newArrayList(); 2916 searchParamBranch.addAll(theSearchParams); 2917 2918 searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); 2919 processNextLinkInChain( 2920 searchParamBranch, 2921 nextSearchParam, 2922 nextChain, 2923 theTargetValue, 2924 qualifiersBranch, 2925 nextQualifier); 2926 } 2927 } 2928 } 2929 } 2930 if (!searchParamFound) { 2931 throw new InvalidRequestException(Msg.code(1214) 2932 + myFhirContext 2933 .getLocalizer() 2934 .getMessage( 2935 BaseStorageDao.class, 2936 "invalidParameterChain", 2937 thePreviousSearchParam.getName() + '.' + theChain)); 2938 } 2939 } 2940 } 2941 2942 private static class LeafNodeDefinition { 2943 private final RuntimeSearchParam myParamDefinition; 2944 private final ArrayList<IQueryParameterType> myOrValues; 2945 private final String myLeafTarget; 2946 private final String myLeafParamName; 2947 private final String myLeafPathPrefix; 2948 private final List<String> myQualifiers; 2949 2950 public LeafNodeDefinition( 2951 RuntimeSearchParam theParamDefinition, 2952 ArrayList<IQueryParameterType> theOrValues, 2953 String theLeafTarget, 2954 String theLeafParamName, 2955 String theLeafPathPrefix, 2956 List<String> theQualifiers) { 2957 myParamDefinition = theParamDefinition; 2958 myOrValues = theOrValues; 2959 myLeafTarget = theLeafTarget; 2960 myLeafParamName = theLeafParamName; 2961 myLeafPathPrefix = theLeafPathPrefix; 2962 myQualifiers = theQualifiers; 2963 } 2964 2965 public RuntimeSearchParam getParamDefinition() { 2966 return myParamDefinition; 2967 } 2968 2969 public ArrayList<IQueryParameterType> getOrValues() { 2970 return myOrValues; 2971 } 2972 2973 public String getLeafTarget() { 2974 return myLeafTarget; 2975 } 2976 2977 public String getLeafParamName() { 2978 return myLeafParamName; 2979 } 2980 2981 public String getLeafPathPrefix() { 2982 return myLeafPathPrefix; 2983 } 2984 2985 public List<String> getQualifiers() { 2986 return myQualifiers; 2987 } 2988 2989 public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { 2990 return new LeafNodeDefinition( 2991 myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); 2992 } 2993 2994 @Override 2995 public boolean equals(Object o) { 2996 if (this == o) return true; 2997 if (o == null || getClass() != o.getClass()) return false; 2998 LeafNodeDefinition that = (LeafNodeDefinition) o; 2999 return Objects.equals(myParamDefinition, that.myParamDefinition) 3000 && Objects.equals(myOrValues, that.myOrValues) 3001 && Objects.equals(myLeafTarget, that.myLeafTarget) 3002 && Objects.equals(myLeafParamName, that.myLeafParamName) 3003 && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) 3004 && Objects.equals(myQualifiers, that.myQualifiers); 3005 } 3006 3007 @Override 3008 public int hashCode() { 3009 return Objects.hash( 3010 myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3011 } 3012 3013 /** 3014 * Return a copy of this object with the given {@link RuntimeSearchParam} 3015 * but all other values unchanged. 3016 */ 3017 public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) { 3018 return new LeafNodeDefinition( 3019 theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 3020 } 3021 } 3022}