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