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