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