
001package ca.uhn.fhir.jpa.search.builder; 002 003/* 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.config.DaoConfig; 028import ca.uhn.fhir.jpa.dao.BaseStorageDao; 029import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; 030import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderToken; 031import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 032import ca.uhn.fhir.jpa.model.config.PartitionSettings; 033import ca.uhn.fhir.jpa.model.entity.ModelConfig; 034import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; 035import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 036import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; 037import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder; 038import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder; 039import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder; 040import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.QuantityBasePredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 047import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 048import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 049import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder; 050import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 051import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 052import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 053import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 054import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 055import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 056import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; 057import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 058import ca.uhn.fhir.jpa.searchparam.util.SourceParam; 059import ca.uhn.fhir.model.api.IQueryParameterAnd; 060import ca.uhn.fhir.model.api.IQueryParameterOr; 061import ca.uhn.fhir.model.api.IQueryParameterType; 062import ca.uhn.fhir.parser.DataFormatException; 063import ca.uhn.fhir.rest.api.Constants; 064import ca.uhn.fhir.rest.api.QualifiedParamList; 065import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 066import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 067import ca.uhn.fhir.rest.api.server.RequestDetails; 068import ca.uhn.fhir.rest.param.CompositeParam; 069import ca.uhn.fhir.rest.param.DateParam; 070import ca.uhn.fhir.rest.param.HasParam; 071import ca.uhn.fhir.rest.param.NumberParam; 072import ca.uhn.fhir.rest.param.ParamPrefixEnum; 073import ca.uhn.fhir.rest.param.QuantityParam; 074import ca.uhn.fhir.rest.param.ReferenceParam; 075import ca.uhn.fhir.rest.param.StringParam; 076import ca.uhn.fhir.rest.param.TokenParam; 077import ca.uhn.fhir.rest.param.TokenParamModifier; 078import ca.uhn.fhir.rest.param.UriParam; 079import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 080import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 081import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 082import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 083import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 084import com.google.common.collect.Lists; 085import com.google.common.collect.Maps; 086import com.google.common.collect.Sets; 087import com.healthmarketscience.sqlbuilder.BinaryCondition; 088import com.healthmarketscience.sqlbuilder.ComboCondition; 089import com.healthmarketscience.sqlbuilder.Condition; 090import com.healthmarketscience.sqlbuilder.Expression; 091import com.healthmarketscience.sqlbuilder.InCondition; 092import com.healthmarketscience.sqlbuilder.OrderObject; 093import com.healthmarketscience.sqlbuilder.SelectQuery; 094import com.healthmarketscience.sqlbuilder.SetOperationQuery; 095import com.healthmarketscience.sqlbuilder.Subquery; 096import com.healthmarketscience.sqlbuilder.UnionQuery; 097import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 098import org.apache.commons.collections4.BidiMap; 099import org.apache.commons.collections4.bidimap.DualHashBidiMap; 100import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap; 101import org.apache.commons.lang3.StringUtils; 102import org.apache.commons.lang3.builder.EqualsBuilder; 103import org.apache.commons.lang3.builder.HashCodeBuilder; 104import org.apache.commons.lang3.tuple.Triple; 105import org.hl7.fhir.instance.model.api.IAnyResource; 106import org.slf4j.Logger; 107import org.slf4j.LoggerFactory; 108 109import javax.annotation.Nonnull; 110import javax.annotation.Nullable; 111import java.math.BigDecimal; 112import java.util.ArrayList; 113import java.util.Arrays; 114import java.util.Collection; 115import java.util.Collections; 116import java.util.EnumSet; 117import java.util.HashMap; 118import java.util.List; 119import java.util.Map; 120import java.util.Objects; 121import java.util.Set; 122import java.util.function.Supplier; 123import java.util.stream.Collectors; 124 125import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 126import static org.apache.commons.lang3.StringUtils.isBlank; 127import static org.apache.commons.lang3.StringUtils.isNotBlank; 128import static org.apache.commons.lang3.StringUtils.split; 129 130public class QueryStack { 131 132 private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class); 133 private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix; 134 135 static { 136 DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>(); 137 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE); 138 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL); 139 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN); 140 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS); 141 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN); 142 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS); 143 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL); 144 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE); 145 compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER); 146 ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix); 147 } 148 149 private final ModelConfig myModelConfig; 150 private final FhirContext myFhirContext; 151 private final SearchQueryBuilder mySqlBuilder; 152 private final SearchParameterMap mySearchParameters; 153 private final ISearchParamRegistry mySearchParamRegistry; 154 private final PartitionSettings myPartitionSettings; 155 private final DaoConfig myDaoConfig; 156 private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes; 157 private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap; 158 159 /** 160 * Constructor 161 */ 162 public QueryStack(SearchParameterMap theSearchParameters, DaoConfig theDaoConfig, ModelConfig theModelConfig, FhirContext theFhirContext, SearchQueryBuilder theSqlBuilder, ISearchParamRegistry theSearchParamRegistry, PartitionSettings thePartitionSettings) { 163 this(theSearchParameters, theDaoConfig, theModelConfig, theFhirContext, theSqlBuilder, theSearchParamRegistry, thePartitionSettings, EnumSet.of(PredicateBuilderTypeEnum.DATE)); 164 } 165 166 /** 167 * Constructor 168 */ 169 private QueryStack(SearchParameterMap theSearchParameters, DaoConfig theDaoConfig, ModelConfig theModelConfig, FhirContext theFhirContext, SearchQueryBuilder theSqlBuilder, ISearchParamRegistry theSearchParamRegistry, PartitionSettings thePartitionSettings, EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) { 170 myPartitionSettings = thePartitionSettings; 171 assert theSearchParameters != null; 172 assert theDaoConfig != null; 173 assert theModelConfig != null; 174 assert theFhirContext != null; 175 assert theSqlBuilder != null; 176 177 mySearchParameters = theSearchParameters; 178 myDaoConfig = theDaoConfig; 179 myModelConfig = theModelConfig; 180 myFhirContext = theFhirContext; 181 mySqlBuilder = theSqlBuilder; 182 mySearchParamRegistry = theSearchParamRegistry; 183 myReusePredicateBuilderTypes = theReusePredicateBuilderTypes; 184 } 185 186 public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) { 187 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 188 DatePredicateBuilder sortPredicateBuilder = mySqlBuilder.addDatePredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 189 190 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 191 mySqlBuilder.addPredicate(hashIdentityPredicate); 192 mySqlBuilder.addSortDate(sortPredicateBuilder.getColumnValueLow(), theAscending); 193 } 194 195 public void addSortOnLastUpdated(boolean theAscending) { 196 ResourceTablePredicateBuilder resourceTablePredicateBuilder; 197 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 198 if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) { 199 resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder; 200 } else { 201 resourceTablePredicateBuilder = mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 202 } 203 mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending); 204 } 205 206 public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) { 207 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 208 NumberPredicateBuilder sortPredicateBuilder = mySqlBuilder.addNumberPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 209 210 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 211 mySqlBuilder.addPredicate(hashIdentityPredicate); 212 mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnValue(), theAscending); 213 } 214 215 public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) { 216 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 217 218 QuantityBasePredicateBuilder sortPredicateBuilder; 219 sortPredicateBuilder = mySqlBuilder.addQuantityPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 220 221 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 222 mySqlBuilder.addPredicate(hashIdentityPredicate); 223 mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnValue(), theAscending); 224 } 225 226 public void addSortOnResourceId(boolean theAscending) { 227 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 228 ForcedIdPredicateBuilder sortPredicateBuilder = mySqlBuilder.addForcedIdPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 229 if (!theAscending) { 230 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnForcedId(), false, OrderObject.NullOrder.FIRST); 231 } else { 232 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnForcedId(), true); 233 } 234 mySqlBuilder.addSortNumeric(firstPredicateBuilder.getResourceIdColumn(), theAscending); 235 236 } 237 238 public void addSortOnResourceLink(String theResourceName, String theParamName, boolean theAscending) { 239 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 240 ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn()); 241 242 Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>()); 243 mySqlBuilder.addPredicate(pathPredicate); 244 mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending); 245 } 246 247 public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) { 248 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 249 StringPredicateBuilder sortPredicateBuilder = mySqlBuilder.addStringPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 250 251 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 252 mySqlBuilder.addPredicate(hashIdentityPredicate); 253 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValueNormalized(), theAscending); 254 } 255 256 public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) { 257 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 258 TokenPredicateBuilder sortPredicateBuilder = mySqlBuilder.addTokenPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 259 260 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 261 mySqlBuilder.addPredicate(hashIdentityPredicate); 262 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnSystem(), theAscending); 263 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValue(), theAscending); 264 } 265 266 public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) { 267 BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 268 UriPredicateBuilder sortPredicateBuilder = mySqlBuilder.addUriPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); 269 270 Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); 271 mySqlBuilder.addPredicate(hashIdentityPredicate); 272 mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValue(), theAscending); 273 } 274 275 @SuppressWarnings("unchecked") 276 private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder(PredicateBuilderTypeEnum theType, DbColumn theSourceJoinColumn, String theParamName, Supplier<T> theFactoryMethod) { 277 boolean cacheHit = false; 278 BaseJoiningPredicateBuilder retVal; 279 if (myReusePredicateBuilderTypes.contains(theType)) { 280 PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName); 281 if (myJoinMap == null) { 282 myJoinMap = new HashMap<>(); 283 } 284 retVal = myJoinMap.get(key); 285 if (retVal != null) { 286 cacheHit = true; 287 } else { 288 retVal = theFactoryMethod.get(); 289 myJoinMap.put(key, retVal); 290 } 291 } else { 292 retVal = theFactoryMethod.get(); 293 } 294 return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal); 295 } 296 297 private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId) { 298 return createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDef, theNextAnd, theRequestPartitionId, mySqlBuilder); 299 } 300 301 private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 302 303 Condition orCondidtion = null; 304 for (IQueryParameterType next : theNextAnd) { 305 306 if (!(next instanceof CompositeParam<?, ?>)) { 307 throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + next.getClass()); 308 } 309 CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next; 310 311 List<RuntimeSearchParam> componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef); 312 RuntimeSearchParam left = componentParams.get(0); 313 IQueryParameterType leftValue = cp.getLeftValue(); 314 Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, left, leftValue, theRequestPartitionId, theSqlBuilder); 315 316 RuntimeSearchParam right = componentParams.get(1); 317 IQueryParameterType rightValue = cp.getRightValue(); 318 Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, right, rightValue, theRequestPartitionId, theSqlBuilder); 319 320 Condition andCondition = toAndPredicate(leftPredicate, rightPredicate); 321 322 if (orCondidtion == null) { 323 orCondidtion = toOrPredicate(andCondition); 324 } else { 325 orCondidtion = toOrPredicate(orCondidtion, andCondition); 326 } 327 } 328 329 return orCondidtion; 330 } 331 332 private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 333 334 switch (theParam.getParamType()) { 335 case STRING: { 336 return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); 337 } 338 case TOKEN: { 339 return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); 340 } 341 case DATE: { 342 return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId, theSqlBuilder); 343 } 344 case QUANTITY: { 345 return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); 346 } 347 case NUMBER: 348 case REFERENCE: 349 case COMPOSITE: 350 case URI: 351 case HAS: 352 case SPECIAL: 353 default: 354 throw new InvalidRequestException(Msg.code(1204) + "Don't know how to handle composite parameter with type of " + theParam.getParamType()); 355 } 356 357 } 358 359 public Condition createPredicateCoords(@Nullable DbColumn theSourceJoinColumn, 360 String theResourceName, 361 RuntimeSearchParam theSearchParam, 362 List<? extends IQueryParameterType> theList, 363 RequestPartitionId theRequestPartitionId) { 364 365 CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.COORDS, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)).getResult(); 366 367 if (theList.get(0).getMissing() != null) { 368 return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); 369 } 370 371 List<Condition> codePredicates = new ArrayList<>(); 372 for (IQueryParameterType nextOr : theList) { 373 Condition singleCode = predicateBuilder.createPredicateCoords(mySearchParameters, nextOr, theResourceName, theSearchParam, predicateBuilder, theRequestPartitionId); 374 codePredicates.add(singleCode); 375 } 376 377 return predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 378 } 379 380 public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 381 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 382 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 383 return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); 384 } 385 public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 386 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 387 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 388 389 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 390 391 PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); 392 DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); 393 boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); 394 395 if (theList.get(0).getMissing() != null) { 396 Boolean missing = theList.get(0).getMissing(); 397 return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, paramName, missing, theRequestPartitionId); 398 } 399 400 List<Condition> codePredicates = new ArrayList<>(); 401 402 for (IQueryParameterType nextOr : theList) { 403 Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation); 404 codePredicates.add(p); 405 } 406 407 Condition predicate = toOrPredicate(codePredicates); 408 409 if (!cacheHit) { 410 predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate); 411 predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 412 } 413 414 return predicate; 415 416 } 417 418 private Condition createPredicateFilter(QueryStack theQueryStack3, SearchFilterParser.Filter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 419 420 if (theFilter instanceof SearchFilterParser.FilterParameter) { 421 return createPredicateFilter(theQueryStack3, (SearchFilterParser.FilterParameter) theFilter, theResourceName, theRequest, theRequestPartitionId); 422 } else if (theFilter instanceof SearchFilterParser.FilterLogical) { 423 // Left side 424 Condition xPredicate = createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterLogical) theFilter).getFilter1(), theResourceName, theRequest, theRequestPartitionId); 425 426 // Right side 427 Condition yPredicate = createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterLogical) theFilter).getFilter2(), theResourceName, theRequest, theRequestPartitionId); 428 429 if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.and) { 430 return ComboCondition.and(xPredicate, yPredicate); 431 } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.or) { 432 return ComboCondition.or(xPredicate, yPredicate); 433 } else { 434 // Shouldn't happen 435 throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation " + ((SearchFilterParser.FilterLogical) theFilter).getOperation()); 436 } 437 } else { 438 return createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), theResourceName, theRequest, theRequestPartitionId); 439 } 440 } 441 442 private Condition createPredicateFilter(QueryStack theQueryStack3, SearchFilterParser.FilterParameter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 443 444 String paramName = theFilter.getParamPath().getName(); 445 446 switch (paramName) { 447 case IAnyResource.SP_RES_ID: { 448 TokenParam param = new TokenParam(); 449 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 450 return theQueryStack3.createPredicateResourceId(null, Collections.singletonList(Collections.singletonList(param)), theResourceName, theFilter.getOperation(), theRequestPartitionId); 451 } 452 case Constants.PARAM_SOURCE: { 453 TokenParam param = new TokenParam(); 454 param.setValueAsQueryToken(null, null, null, theFilter.getValue()); 455 return createPredicateSource(null, Collections.singletonList(param)); 456 } 457 default: 458 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramName); 459 if (searchParam == null) { 460 Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName); 461 String msg = myFhirContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", paramName, theResourceName, validNames); 462 throw new InvalidRequestException(Msg.code(1206) + msg); 463 } 464 RestSearchParameterTypeEnum typeEnum = searchParam.getParamType(); 465 if (typeEnum == RestSearchParameterTypeEnum.URI) { 466 return theQueryStack3.createPredicateUri(null, theResourceName, null, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequest, theRequestPartitionId); 467 } else if (typeEnum == RestSearchParameterTypeEnum.STRING) { 468 return theQueryStack3.createPredicateString(null, theResourceName, null, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); 469 } else if (typeEnum == RestSearchParameterTypeEnum.DATE) { 470 return theQueryStack3.createPredicateDate(null, theResourceName, null, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); 471 } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) { 472 return theQueryStack3.createPredicateNumber(null, theResourceName, null, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); 473 } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) { 474 SearchFilterParser.CompareOperation operation = theFilter.getOperation(); 475 String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here 476 String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null; 477 String value = theFilter.getValue(); 478 ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value); 479 return theQueryStack3.createPredicateReference(null, theResourceName, paramName, new ArrayList<>(), Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId); 480 } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) { 481 return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); 482 } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) { 483 throw new InvalidRequestException(Msg.code(1207) + "Composite search parameters not currently supported with _filter clauses"); 484 } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) { 485 TokenParam param = new TokenParam(); 486 param.setValueAsQueryToken(null, 487 null, 488 null, 489 theFilter.getValue()); 490 return theQueryStack3.createPredicateToken(null, theResourceName, null, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId); 491 } 492 break; 493 } 494 return null; 495 } 496 497 private Condition createPredicateHas(@Nullable DbColumn theSourceJoinColumn, String theResourceType, List<List<IQueryParameterType>> theHasParameters, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 498 499 List<Condition> andPredicates = new ArrayList<>(); 500 for (List<? extends IQueryParameterType> nextOrList : theHasParameters) { 501 502 String targetResourceType = null; 503 String paramReference = null; 504 String parameterName = null; 505 506 String paramName = null; 507 List<QualifiedParamList> parameters = new ArrayList<>(); 508 for (IQueryParameterType nextParam : nextOrList) { 509 HasParam next = (HasParam) nextParam; 510 targetResourceType = next.getTargetResourceType(); 511 paramReference = next.getReferenceFieldName(); 512 parameterName = next.getParameterName(); 513 paramName = parameterName.replaceAll("\\..*", ""); 514 parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext))); 515 } 516 517 if (paramName == null) { 518 continue; 519 } 520 521 try { 522 myFhirContext.getResourceDefinition(targetResourceType); 523 } catch (DataFormatException e) { 524 throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType); 525 } 526 527 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 528 529 if (paramName.startsWith("_has:")) { 530 531 ourLog.trace("Handing double _has query: {}", paramName); 532 533 String qualifier = paramName.substring(4); 534 for (IQueryParameterType next : nextOrList) { 535 HasParam nextHasParam = new HasParam(); 536 nextHasParam.setValueAsQueryToken(myFhirContext, Constants.PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext)); 537 orValues.add(nextHasParam); 538 } 539 540 } else { 541 542 //Ensure that the name of the search param 543 // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val) 544 // exists on the target resource type. 545 RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramName); 546 547 //Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in Patient?_has:Observation:subject:code=sys|val) 548 //exists on the target resource, or in the top-level Resource resource. 549 mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramReference); 550 551 552 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters); 553 554 for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) { 555 orValues.addAll(next.getValuesAsQueryTokens()); 556 } 557 558 } 559 560 //Handle internal chain inside the has. 561 if (parameterName.contains(".")) { 562 String chainedPartOfParameter = getChainedPart(parameterName); 563 orValues.stream() 564 .filter(qp -> qp instanceof ReferenceParam) 565 .map(qp -> (ReferenceParam) qp) 566 .forEach(rp -> rp.setChain(getChainedPart(chainedPartOfParameter))); 567 568 parameterName = parameterName.substring(0, parameterName.indexOf('.')); 569 } 570 571 int colonIndex = parameterName.indexOf(':'); 572 if (colonIndex != -1) { 573 parameterName = parameterName.substring(0, colonIndex); 574 } 575 576 ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn); 577 Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId); 578 579 List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference, new ArrayList<>()); 580 Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType)); 581 Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); 582 Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE); 583 andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate)); 584 } 585 586 return toAndPredicate(andPredicates); 587 } 588 589 public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 590 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 591 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 592 return createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); 593 } 594 595 public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 596 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 597 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 598 599 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 600 601 NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult(); 602 603 if (theList.get(0).getMissing() != null) { 604 return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 605 } 606 607 List<Condition> codePredicates = new ArrayList<>(); 608 for (IQueryParameterType nextOr : theList) { 609 610 if (nextOr instanceof NumberParam) { 611 NumberParam param = (NumberParam) nextOr; 612 613 BigDecimal value = param.getValue(); 614 if (value == null) { 615 continue; 616 } 617 618 SearchFilterParser.CompareOperation operation = theOperation; 619 if (operation == null) { 620 operation = toOperation(param.getPrefix()); 621 } 622 623 624 Condition predicate = join.createPredicateNumeric(theResourceName, paramName, operation, value, theRequestPartitionId, nextOr); 625 codePredicates.add(predicate); 626 627 } else { 628 throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass()); 629 } 630 631 } 632 633 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 634 } 635 636 public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 637 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 638 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 639 return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); 640 } 641 642 public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 643 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 644 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 645 646 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 647 648 if (theList.get(0).getMissing() != null) { 649 QuantityBasePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); 650 return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 651 } 652 653 List<QuantityParam> quantityParams = theList 654 .stream() 655 .map(t -> QuantityParam.toQuantityParam(t)) 656 .collect(Collectors.toList()); 657 658 QuantityBasePredicateBuilder join = null; 659 boolean normalizedSearchEnabled = myModelConfig.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED); 660 if (normalizedSearchEnabled) { 661 List<QuantityParam> normalizedQuantityParams = quantityParams 662 .stream() 663 .map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t)) 664 .filter(t -> t != null) 665 .collect(Collectors.toList()); 666 667 if (normalizedQuantityParams.size() == quantityParams.size()) { 668 join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); 669 quantityParams = normalizedQuantityParams; 670 } 671 } 672 673 if (join == null) { 674 join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); 675 } 676 677 List<Condition> codePredicates = new ArrayList<>(); 678 for (QuantityParam nextOr : quantityParams) { 679 Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId); 680 codePredicates.add(singleCode); 681 } 682 683 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0]))); 684 } 685 686 public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn, 687 String theResourceName, 688 String theParamName, 689 List<String> theQualifiers, 690 List<? extends IQueryParameterType> theList, 691 SearchFilterParser.CompareOperation theOperation, 692 RequestDetails theRequest, 693 RequestPartitionId theRequestPartitionId) { 694 return createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequest, theRequestPartitionId, mySqlBuilder); 695 } 696 697 public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn, 698 String theResourceName, 699 String theParamName, 700 List<String> theQualifiers, 701 List<? extends IQueryParameterType> theList, 702 SearchFilterParser.CompareOperation theOperation, 703 RequestDetails theRequest, 704 RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 705 706 if ((theOperation != null) && 707 (theOperation != SearchFilterParser.CompareOperation.eq) && 708 (theOperation != SearchFilterParser.CompareOperation.ne)) { 709 throw new InvalidRequestException(Msg.code(1212) + "Invalid operator specified for reference predicate. Supported operators for reference predicate are \"eq\" and \"ne\"."); 710 } 711 712 if (theList.get(0).getMissing() != null) { 713 SearchParamPresentPredicateBuilder join = theSqlBuilder.addSearchParamPresentPredicateBuilder(theSourceJoinColumn); 714 return join.createPredicateParamMissingForReference(theResourceName, theParamName, theList.get(0).getMissing(), theRequestPartitionId); 715 716 } 717 718 ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult(); 719 return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId); 720 } 721 722 private class ChainElement { 723 private final String myResourceType; 724 private final String mySearchParameterName; 725 private final String myPath; 726 727 public ChainElement(String theResourceType, String theSearchParameterName, String thePath) { 728 this.myResourceType = theResourceType; 729 this.mySearchParameterName = theSearchParameterName; 730 this.myPath = thePath; 731 } 732 733 public String getResourceType() { 734 return myResourceType; 735 } 736 737 public String getPath() { return myPath; } 738 739 public String getSearchParameterName() { return mySearchParameterName; } 740 741 @Override 742 public boolean equals(Object o) { 743 if (this == o) return true; 744 if (o == null || getClass() != o.getClass()) return false; 745 ChainElement that = (ChainElement) o; 746 return myResourceType.equals(that.myResourceType) && mySearchParameterName.equals(that.mySearchParameterName) && myPath.equals(that.myPath); 747 } 748 749 @Override 750 public int hashCode() { 751 return Objects.hash(myResourceType, mySearchParameterName, myPath); 752 } 753 } 754 755 private class ReferenceChainExtractor { 756 private final Map<List<ChainElement>,Set<LeafNodeDefinition>> myChains = Maps.newHashMap(); 757 758 public Map<List<ChainElement>,Set<LeafNodeDefinition>> getChains() { return myChains; } 759 760 private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { 761 return split(theReferenceParam.getChain(), '.').length <= 3; 762 } 763 764 private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) { 765 List<String> pathsForType = theSearchParam.getPathsSplit().stream() 766 .map(String::trim) 767 .filter(t -> t.startsWith(theResourceType)) 768 .collect(Collectors.toList()); 769 if (pathsForType.isEmpty()) { 770 ourLog.warn("Search parameter {} does not have a path for resource type {}.", theSearchParam.getName(), theResourceType); 771 } 772 773 return pathsForType; 774 } 775 776 public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList) { 777 List<String> paths = extractPaths(theResourceType, theSearchParam); 778 for (String path : paths) { 779 List<ChainElement> searchParams = Lists.newArrayList(); 780 searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path)); 781 for (IQueryParameterType nextOr : theList) { 782 String targetValue = nextOr.getValueAsQueryToken(myFhirContext); 783 if (nextOr instanceof ReferenceParam) { 784 ReferenceParam referenceParam = (ReferenceParam) nextOr; 785 786 if (!isReferenceParamValid(referenceParam)) { 787 throw new InvalidRequestException(Msg.code(2007) + 788 "The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() + 789 " is too long. Only chains up to three references are supported."); 790 } 791 792 String targetChain = referenceParam.getChain(); 793 List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType()); 794 795 processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType()); 796 797 } 798 } 799 } 800 } 801 802 private void processNextLinkInChain(List<ChainElement> theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List<String> theQualifiers, String theResourceType) { 803 804 String nextParamName = theChain; 805 String nextChain = null; 806 String nextQualifier = null; 807 int linkIndex = theChain.indexOf('.'); 808 if (linkIndex != -1) { 809 nextParamName = theChain.substring(0, linkIndex); 810 nextChain = theChain.substring(linkIndex+1); 811 } 812 813 int qualifierIndex = nextParamName.indexOf(':'); 814 if (qualifierIndex != -1) { 815 nextParamName = nextParamName.substring(0, qualifierIndex); 816 nextQualifier = nextParamName.substring(qualifierIndex); 817 } 818 819 List<String> qualifiersBranch = Lists.newArrayList(); 820 qualifiersBranch.addAll(theQualifiers); 821 qualifiersBranch.add(nextQualifier); 822 823 boolean searchParamFound = false; 824 for (String nextTarget : thePreviousSearchParam.getTargets()) { 825 RuntimeSearchParam nextSearchParam = null; 826 if (StringUtils.isBlank(theResourceType) || theResourceType.equals(nextTarget)) { 827 nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName); 828 } 829 if (nextSearchParam != null) { 830 searchParamFound = true; 831 // If we find a search param on this resource type for this parameter name, keep iterating 832 // Otherwise, abandon this branch and carry on to the next one 833 if (StringUtils.isEmpty(nextChain)) { 834 // We've reached the end of the chain 835 ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); 836 837 if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { 838 orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); 839 } else { 840 IQueryParameterType qp = toParameterType(nextSearchParam); 841 qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); 842 orValues.add(qp); 843 } 844 845 Set<LeafNodeDefinition> leafNodes = myChains.get(theSearchParams); 846 if (leafNodes == null) { 847 leafNodes = Sets.newHashSet(); 848 myChains.put(theSearchParams, leafNodes); 849 } 850 leafNodes.add(new LeafNodeDefinition(nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); 851 } else { 852 List<String> nextPaths = extractPaths(nextTarget, nextSearchParam); 853 for (String nextPath : nextPaths) { 854 List<ChainElement> searchParamBranch = Lists.newArrayList(); 855 searchParamBranch.addAll(theSearchParams); 856 857 searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath)); 858 processNextLinkInChain(searchParamBranch, nextSearchParam, nextChain, theTargetValue, qualifiersBranch, nextQualifier); 859 } 860 } 861 } 862 } 863 if (!searchParamFound) { 864 throw new InvalidRequestException(Msg.code(1214) + myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", thePreviousSearchParam.getName() + '.' + theChain)); 865 } 866 } 867 } 868 869 private static class LeafNodeDefinition { 870 private final RuntimeSearchParam myParamDefinition; 871 private final ArrayList<IQueryParameterType> myOrValues; 872 private final String myLeafTarget; 873 private final String myLeafParamName; 874 private final String myLeafPathPrefix; 875 private final List<String> myQualifiers; 876 877 public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List<String> theQualifiers) { 878 myParamDefinition = theParamDefinition; 879 myOrValues = theOrValues; 880 myLeafTarget = theLeafTarget; 881 myLeafParamName = theLeafParamName; 882 myLeafPathPrefix = theLeafPathPrefix; 883 myQualifiers = theQualifiers; 884 } 885 886 public RuntimeSearchParam getParamDefinition() { 887 return myParamDefinition; 888 } 889 890 public ArrayList<IQueryParameterType> getOrValues() { 891 return myOrValues; 892 } 893 894 public String getLeafTarget() { 895 return myLeafTarget; 896 } 897 898 public String getLeafParamName() { 899 return myLeafParamName; 900 } 901 902 public String getLeafPathPrefix() { 903 return myLeafPathPrefix; 904 } 905 906 public List<String> getQualifiers() { 907 return myQualifiers; 908 } 909 910 public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { 911 return new LeafNodeDefinition(myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); 912 } 913 914 @Override 915 public boolean equals(Object o) { 916 if (this == o) return true; 917 if (o == null || getClass() != o.getClass()) return false; 918 LeafNodeDefinition that = (LeafNodeDefinition) o; 919 return Objects.equals(myParamDefinition, that.myParamDefinition) && Objects.equals(myOrValues, that.myOrValues) && Objects.equals(myLeafTarget, that.myLeafTarget) && Objects.equals(myLeafParamName, that.myLeafParamName) && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) && Objects.equals(myQualifiers, that.myQualifiers); 920 } 921 922 @Override 923 public int hashCode() { 924 return Objects.hash(myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); 925 } 926 } 927 928 public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, 929 String theResourceName, RuntimeSearchParam theSearchParam, 930 List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation, 931 RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 932 // 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 builders across different subselects 933 EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes); 934 myReusePredicateBuilderTypes.clear(); 935 936 UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION_ALL); 937 938 ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); 939 chainExtractor.deriveChains(theResourceName, theSearchParam, theList); 940 Map<List<ChainElement>,Set<LeafNodeDefinition>> chains = chainExtractor.getChains(); 941 942 Map<List<String>,Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap(); 943 for (List<ChainElement> nextChain : chains.keySet()) { 944 Set<LeafNodeDefinition> leafNodes = chains.get(nextChain); 945 946 collateChainedSearchOptions(referenceLinks, nextChain, leafNodes); 947 } 948 949 for (List<String> nextReferenceLink: referenceLinks.keySet()) { 950 for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { 951 SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder(); 952 DbColumn previousJoinColumn = null; 953 954 // Create a reference link predicate to the subselect for every link but the last one 955 for (String nextLink : nextReferenceLink) { 956 // We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion. 957 // TODO: Are we missing any important business logic from that method? All tests are passing. 958 ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = builder.addReferencePredicateBuilder(this, previousJoinColumn); 959 builder.addPredicate(resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink))); 960 previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId(); 961 } 962 963 Condition containedCondition = createIndexPredicate( 964 previousJoinColumn, 965 leafNodeDefinition.getLeafTarget(), 966 leafNodeDefinition.getLeafPathPrefix(), 967 leafNodeDefinition.getLeafParamName(), 968 leafNodeDefinition.getParamDefinition(), 969 leafNodeDefinition.getOrValues(), 970 theOperation, 971 leafNodeDefinition.getQualifiers(), 972 theRequest, 973 theRequestPartitionId, 974 builder); 975 976 builder.addPredicate(containedCondition); 977 978 union.addQueries(builder.getSelect()); 979 } 980 } 981 982 InCondition inCondition; 983 if (theSourceJoinColumn == null) { 984 inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); 985 } else { 986 //-- for the resource link, need join with target_resource_id 987 inCondition = new InCondition(theSourceJoinColumn, union); 988 } 989 990 // restore the state of this collection to turn caching back on before we exit 991 myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); 992 return inCondition; 993 } 994 995 private void collateChainedSearchOptions(Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, List<ChainElement> nextChain, Set<LeafNodeDefinition> leafNodes) { 996 // Manually collapse the chain using all possible variants of contained resource patterns. 997 // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this someday? 998 // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper support for `_contained` 999 if (nextChain.size() == 1) { 1000 // discrete -> discrete 1001 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes); 1002 // discrete -> contained 1003 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), 1004 leafNodes 1005 .stream() 1006 .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName())) 1007 .collect(Collectors.toSet())); 1008 } else if (nextChain.size() == 2) { 1009 // discrete -> discrete -> discrete 1010 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath()), leafNodes); 1011 // discrete -> discrete -> contained 1012 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), 1013 leafNodes 1014 .stream() 1015 .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParameterName())) 1016 .collect(Collectors.toSet())); 1017 // discrete -> contained -> discrete 1018 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath())), leafNodes); 1019 if (myModelConfig.isIndexOnContainedResourcesRecursively()) { 1020 // discrete -> contained -> contained 1021 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), 1022 leafNodes 1023 .stream() 1024 .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName() + "." + nextChain.get(1).getSearchParameterName())) 1025 .collect(Collectors.toSet())); 1026 } 1027 } else if (nextChain.size() == 3) { 1028 // discrete -> discrete -> discrete -> discrete 1029 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath(), nextChain.get(2).getPath()), leafNodes); 1030 // discrete -> discrete -> discrete -> contained 1031 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath()), 1032 leafNodes 1033 .stream() 1034 .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParameterName())) 1035 .collect(Collectors.toSet())); 1036 // discrete -> discrete -> contained -> discrete 1037 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), mergePaths(nextChain.get(1).getPath(), nextChain.get(2).getPath())), leafNodes); 1038 // discrete -> contained -> discrete -> discrete 1039 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath()), nextChain.get(2).getPath()), leafNodes); 1040 // discrete -> contained -> discrete -> contained 1041 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath())), 1042 leafNodes 1043 .stream() 1044 .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParameterName())) 1045 .collect(Collectors.toSet())); 1046 if (myModelConfig.isIndexOnContainedResourcesRecursively()) { 1047 // discrete -> contained -> contained -> discrete 1048 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath(), nextChain.get(2).getPath())), leafNodes); 1049 // discrete -> discrete -> contained -> contained 1050 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), 1051 leafNodes 1052 .stream() 1053 .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParameterName() + "." + nextChain.get(2).getSearchParameterName())) 1054 .collect(Collectors.toSet())); 1055 // discrete -> contained -> contained -> contained 1056 updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), 1057 leafNodes 1058 .stream() 1059 .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName() + "." + nextChain.get(1).getSearchParameterName() + "." + nextChain.get(2).getSearchParameterName())) 1060 .collect(Collectors.toSet())); 1061 } 1062 } else { 1063 // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever needs this, we should revisit the approach 1064 throw new InvalidRequestException(Msg.code(2011) + 1065 "The search chain is too long. Only chains of up to three references are supported."); 1066 } 1067 } 1068 1069 private void updateMapOfReferenceLinks(Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, ArrayList<String> thePath, Set<LeafNodeDefinition> theLeafNodesToAdd) { 1070 Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.get(thePath); 1071 if (leafNodes == null) { 1072 leafNodes = Sets.newHashSet(); 1073 theReferenceLinksMap.put(thePath, leafNodes); 1074 } 1075 leafNodes.addAll(theLeafNodesToAdd); 1076 } 1077 1078 private String mergePaths(String... paths) { 1079 String result = ""; 1080 for (String nextPath : paths) { 1081 int separatorIndex = nextPath.indexOf('.'); 1082 if (StringUtils.isEmpty(result)) { 1083 result = nextPath; 1084 } else { 1085 result = result + nextPath.substring(separatorIndex); 1086 } 1087 } 1088 return result; 1089 } 1090 1091 private Condition createIndexPredicate(DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, String theParamName, RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, SearchFilterParser.CompareOperation theOperation, List<String> theQualifiers, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 1092 Condition containedCondition; 1093 1094 switch (theParamDefinition.getParamType()) { 1095 case DATE: 1096 containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1097 theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); 1098 break; 1099 case NUMBER: 1100 containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1101 theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); 1102 break; 1103 case QUANTITY: 1104 containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1105 theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); 1106 break; 1107 case STRING: 1108 containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1109 theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); 1110 break; 1111 case TOKEN: 1112 containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1113 theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); 1114 break; 1115 case COMPOSITE: 1116 containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1117 theOrValues, theRequestPartitionId, theSqlBuilder); 1118 break; 1119 case URI: 1120 containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, 1121 theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); 1122 break; 1123 case REFERENCE: 1124 containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers, 1125 theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); 1126 break; 1127 case HAS: 1128 case SPECIAL: 1129 default: 1130 throw new InvalidRequestException( 1131 Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported."); 1132 } 1133 return containedCondition; 1134 } 1135 1136 @Nullable 1137 public Condition createPredicateResourceId(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 1138 ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder(); 1139 return builder.createPredicateResourceId(theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId); 1140 } 1141 1142 private Condition createPredicateSourceForAndList(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) { 1143 List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size()); 1144 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1145 andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd)); 1146 } 1147 return toAndPredicate(andPredicates); 1148 } 1149 1150 private Condition createPredicateSource(@Nullable DbColumn theSourceJoinColumn, List<? extends IQueryParameterType> theList) { 1151 if (myDaoConfig.getStoreMetaSourceInformation() == DaoConfig.StoreMetaSourceInformationEnum.NONE) { 1152 String msg = myFhirContext.getLocalizer().getMessage(LegacySearchBuilder.class, "sourceParamDisabled"); 1153 throw new InvalidRequestException(Msg.code(1216) + msg); 1154 } 1155 1156 SourcePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.SOURCE, theSourceJoinColumn, Constants.PARAM_SOURCE, () -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn)).getResult(); 1157 1158 List<Condition> orPredicates = new ArrayList<>(); 1159 for (IQueryParameterType nextParameter : theList) { 1160 SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext)); 1161 String sourceUri = sourceParameter.getSourceUri(); 1162 String requestId = sourceParameter.getRequestId(); 1163 if (isNotBlank(sourceUri) && isNotBlank(requestId)) { 1164 orPredicates.add(toAndPredicate( 1165 join.createPredicateSourceUri(sourceUri), 1166 join.createPredicateRequestId(requestId) 1167 )); 1168 } else if (isNotBlank(sourceUri)) { 1169 orPredicates.add(join.createPredicateSourceUri(sourceUri)); 1170 } else if (isNotBlank(requestId)) { 1171 orPredicates.add(join.createPredicateRequestId(requestId)); 1172 } 1173 } 1174 1175 return toOrPredicate(orPredicates); 1176 } 1177 1178 public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1179 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1180 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 1181 return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); 1182 } 1183 1184 public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1185 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1186 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, 1187 SearchQueryBuilder theSqlBuilder) { 1188 1189 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1190 1191 StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult(); 1192 1193 if (theList.get(0).getMissing() != null) { 1194 return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 1195 } 1196 1197 List<Condition> codePredicates = new ArrayList<>(); 1198 for (IQueryParameterType nextOr : theList) { 1199 Condition singleCode = join.createPredicateString(nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation); 1200 codePredicates.add(singleCode); 1201 } 1202 1203 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates)); 1204 } 1205 1206 public Condition createPredicateTag(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theList, String theParamName, RequestPartitionId theRequestPartitionId) { 1207 TagTypeEnum tagType; 1208 if (Constants.PARAM_TAG.equals(theParamName)) { 1209 tagType = TagTypeEnum.TAG; 1210 } else if (Constants.PARAM_PROFILE.equals(theParamName)) { 1211 tagType = TagTypeEnum.PROFILE; 1212 } else if (Constants.PARAM_SECURITY.equals(theParamName)) { 1213 tagType = TagTypeEnum.SECURITY_LABEL; 1214 } else { 1215 throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen 1216 } 1217 1218 List<Condition> andPredicates = new ArrayList<>(); 1219 for (List<? extends IQueryParameterType> nextAndParams : theList) { 1220 if ( ! checkHaveTags(nextAndParams, theParamName)) { continue; } 1221 1222 List<Triple<String, String, String>> tokens = Lists.newArrayList(); 1223 boolean paramInverted = populateTokens(tokens, nextAndParams); 1224 if (tokens.isEmpty()) { continue; } 1225 1226 Condition tagPredicate; 1227 BaseJoiningPredicateBuilder join; 1228 if (paramInverted) { 1229 1230 SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(); 1231 TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null); 1232 sqlBuilder.addPredicate(tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId)); 1233 SelectQuery sql = sqlBuilder.getSelect(); 1234 1235 join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1236 Expression subSelect = new Subquery(sql); 1237 tagPredicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); 1238 1239 } else { 1240 // Tag table can't be a query root because it will include deleted resources, and can't select by resource type 1241 mySqlBuilder.getOrCreateFirstPredicateBuilder(); 1242 1243 TagPredicateBuilder tagJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TAG, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn)).getResult(); 1244 tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId); 1245 join = tagJoin; 1246 } 1247 1248 andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate)); 1249 } 1250 1251 return toAndPredicate(andPredicates); 1252 } 1253 1254 private boolean populateTokens(List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) { 1255 boolean paramInverted = false; 1256 1257 for (IQueryParameterType nextOrParam : theAndParams) { 1258 String code; 1259 String system; 1260 if (nextOrParam instanceof TokenParam) { 1261 TokenParam nextParam = (TokenParam) nextOrParam; 1262 code = nextParam.getValue(); 1263 system = nextParam.getSystem(); 1264 if (nextParam.getModifier() == TokenParamModifier.NOT) { 1265 paramInverted = true; 1266 } 1267 } else { 1268 UriParam nextParam = (UriParam) nextOrParam; 1269 code = nextParam.getValue(); 1270 system = null; 1271 } 1272 1273 if (isNotBlank(code)) { 1274 theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code)); 1275 } 1276 } 1277 return paramInverted; 1278 } 1279 1280 private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) { 1281 for (IQueryParameterType nextParamUncasted : theParams) { 1282 if (nextParamUncasted instanceof TokenParam) { 1283 TokenParam nextParam = (TokenParam) nextParamUncasted; 1284 if (isNotBlank(nextParam.getValue())) { return true; } 1285 if (isNotBlank(nextParam.getSystem())) { 1286 throw new InvalidRequestException(Msg.code(1218) + "Invalid " + theParamName + 1287 " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myFhirContext)); 1288 } 1289 } 1290 1291 UriParam nextParam = (UriParam) nextParamUncasted; 1292 if (isNotBlank(nextParam.getValue())) { return true; } 1293 } 1294 1295 return false; 1296 } 1297 1298 public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1299 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1300 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 1301 return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); 1302 } 1303 1304 public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1305 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1306 SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 1307 1308 List<IQueryParameterType> tokens = new ArrayList<>(); 1309 1310 boolean paramInverted = false; 1311 TokenParamModifier modifier; 1312 1313 for (IQueryParameterType nextOr : theList) { 1314 if (nextOr instanceof TokenParam) { 1315 if (!((TokenParam) nextOr).isEmpty()) { 1316 TokenParam id = (TokenParam) nextOr; 1317 if (id.isText()) { 1318 1319 // Check whether the :text modifier is actually enabled here 1320 boolean tokenTextIndexingEnabled = BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam); 1321 if (!tokenTextIndexingEnabled) { 1322 String msg; 1323 if (myModelConfig.isSuppressStringIndexingInTokens()) { 1324 msg = myFhirContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForServer"); 1325 } else { 1326 msg = myFhirContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForSearchParam"); 1327 } 1328 throw new MethodNotAllowedException(Msg.code(1219) + msg); 1329 } 1330 1331 return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId, theSqlBuilder); 1332 } 1333 1334 modifier = id.getModifier(); 1335 // for :not modifier, create a token and remove the :not modifier 1336 if (modifier == TokenParamModifier.NOT) { 1337 tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue())); 1338 paramInverted = true; 1339 } else { 1340 tokens.add(nextOr); 1341 } 1342 } 1343 } else { 1344 tokens.add(nextOr); 1345 } 1346 } 1347 1348 if (tokens.isEmpty()) { 1349 return null; 1350 } 1351 1352 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1353 Condition predicate; 1354 BaseJoiningPredicateBuilder join; 1355 1356 if (paramInverted) { 1357 SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(); 1358 TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); 1359 sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); 1360 SelectQuery sql = sqlBuilder.getSelect(); 1361 Expression subSelect = new Subquery(sql); 1362 1363 join = theSqlBuilder.getOrCreateFirstPredicateBuilder(); 1364 1365 if (theSourceJoinColumn == null) { 1366 predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); 1367 } else { 1368 //-- for the resource link, need join with target_resource_id 1369 predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true); 1370 } 1371 1372 } else { 1373 1374 TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult(); 1375 1376 if (theList.get(0).getMissing() != null) { 1377 return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 1378 } 1379 1380 predicate = tokenJoin.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId); 1381 join = tokenJoin; 1382 } 1383 1384 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 1385 } 1386 1387 public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1388 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1389 SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, 1390 RequestPartitionId theRequestPartitionId) { 1391 return createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestDetails, theRequestPartitionId, mySqlBuilder); 1392 } 1393 1394 public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName, 1395 String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, 1396 SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, 1397 RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { 1398 1399 String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 1400 1401 UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); 1402 1403 if (theList.get(0).getMissing() != null) { 1404 return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 1405 } 1406 1407 Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails); 1408 return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate); 1409 } 1410 1411 public QueryStack newChildQueryFactoryWithFullBuilderReuse() { 1412 return new QueryStack(mySearchParameters, myDaoConfig, myModelConfig, myFhirContext, mySqlBuilder, mySearchParamRegistry, myPartitionSettings, EnumSet.allOf(PredicateBuilderTypeEnum.class)); 1413 } 1414 1415 @Nullable 1416 public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchContainedModeEnum theSearchContainedMode) { 1417 1418 if (theAndOrParams.isEmpty()) { 1419 return null; 1420 } 1421 1422 switch (theParamName) { 1423 case IAnyResource.SP_RES_ID: 1424 return createPredicateResourceId(theSourceJoinColumn, theAndOrParams, theResourceName, null, theRequestPartitionId); 1425 1426 case Constants.PARAM_HAS: 1427 return createPredicateHas(theSourceJoinColumn, theResourceName, theAndOrParams, theRequest, theRequestPartitionId); 1428 1429 case Constants.PARAM_TAG: 1430 case Constants.PARAM_PROFILE: 1431 case Constants.PARAM_SECURITY: 1432 if (myDaoConfig.getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE) { 1433 return createPredicateSearchParameter(theSourceJoinColumn, theResourceName, theParamName, theAndOrParams, theRequest, theRequestPartitionId); 1434 } else { 1435 return createPredicateTag(theSourceJoinColumn, theAndOrParams, theParamName, theRequestPartitionId); 1436 } 1437 1438 case Constants.PARAM_SOURCE: 1439 return createPredicateSourceForAndList(theSourceJoinColumn, theAndOrParams); 1440 1441 default: 1442 return createPredicateSearchParameter(theSourceJoinColumn, theResourceName, theParamName, theAndOrParams, theRequest, theRequestPartitionId); 1443 1444 } 1445 1446 } 1447 1448 @Nullable 1449 private Condition createPredicateSearchParameter(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 1450 List<Condition> andPredicates = new ArrayList<>(); 1451 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); 1452 if (nextParamDef != null) { 1453 1454 if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) { 1455 if (theRequestPartitionId.isAllPartitions()) { 1456 throw new PreconditionFailedException(Msg.code(1220) + "This server is not configured to support search against all partitions"); 1457 } 1458 } 1459 1460 switch (nextParamDef.getParamType()) { 1461 case DATE: 1462 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1463 // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt' 1464 // to create the predicateDate instead of generic one with operation = null 1465 SearchFilterParser.CompareOperation operation = null; 1466 if (nextAnd.size() > 0) { 1467 DateParam param = (DateParam) nextAnd.get(0); 1468 operation = toOperation(param.getPrefix()); 1469 } 1470 andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId)); 1471 } 1472 break; 1473 case QUANTITY: 1474 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1475 SearchFilterParser.CompareOperation operation = null; 1476 if (nextAnd.size() > 0) { 1477 QuantityParam param = (QuantityParam) nextAnd.get(0); 1478 operation = toOperation(param.getPrefix()); 1479 } 1480 andPredicates.add(createPredicateQuantity(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId)); 1481 } 1482 break; 1483 case REFERENCE: 1484 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1485 if (isEligibleForContainedResourceSearch(nextAnd)) { 1486 andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); 1487 } else { 1488 andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); 1489 } 1490 } 1491 break; 1492 case STRING: 1493 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1494 andPredicates.add(createPredicateString(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.sw, theRequestPartitionId)); 1495 } 1496 break; 1497 case TOKEN: 1498 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1499 if ("Location.position".equals(nextParamDef.getPath())) { 1500 andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId)); 1501 } else { 1502 andPredicates.add(createPredicateToken(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId)); 1503 } 1504 } 1505 break; 1506 case NUMBER: 1507 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1508 andPredicates.add(createPredicateNumber(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId)); 1509 } 1510 break; 1511 case COMPOSITE: 1512 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1513 andPredicates.add(createPredicateComposite(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, theRequestPartitionId)); 1514 } 1515 break; 1516 case URI: 1517 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1518 andPredicates.add(createPredicateUri(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.eq, theRequest, theRequestPartitionId)); 1519 } 1520 break; 1521 case HAS: 1522 case SPECIAL: 1523 for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { 1524 if ("Location.position".equals(nextParamDef.getPath())) { 1525 andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId)); 1526 } 1527 } 1528 break; 1529 } 1530 } else { 1531 // These are handled later 1532 if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) { 1533 if (Constants.PARAM_FILTER.equals(theParamName)) { 1534 1535 // Parse the predicates enumerated in the _filter separated by AND or OR... 1536 if (theAndOrParams.get(0).get(0) instanceof StringParam) { 1537 String filterString = ((StringParam) theAndOrParams.get(0).get(0)).getValue(); 1538 SearchFilterParser.Filter filter; 1539 try { 1540 filter = SearchFilterParser.parse(filterString); 1541 } catch (SearchFilterParser.FilterSyntaxException theE) { 1542 throw new InvalidRequestException(Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage()); 1543 } 1544 if (filter != null) { 1545 1546 if (!myDaoConfig.isFilterParameterEnabled()) { 1547 throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER + " parameter is disabled on this server"); 1548 } 1549 1550 Condition predicate = createPredicateFilter(this, filter, theResourceName, theRequest, theRequestPartitionId); 1551 if (predicate != null) { 1552 mySqlBuilder.addPredicate(predicate); 1553 } 1554 } 1555 } 1556 1557 } else { 1558 String msg = myFhirContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", theParamName, theResourceName, mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName)); 1559 throw new InvalidRequestException(Msg.code(1223) + msg); 1560 } 1561 } 1562 } 1563 1564 return toAndPredicate(andPredicates); 1565 } 1566 1567 private boolean isEligibleForContainedResourceSearch(List<? extends IQueryParameterType> nextAnd) { 1568 return myModelConfig.isIndexOnContainedResources() && 1569 nextAnd.stream() 1570 .filter(t -> t instanceof ReferenceParam) 1571 .map(t -> ((ReferenceParam) t).getChain()) 1572 .anyMatch(StringUtils::isNotBlank); 1573 } 1574 1575 public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { 1576 ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder(); 1577 Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString); 1578 mySqlBuilder.addPredicate(predicate); 1579 } 1580 1581 public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { 1582 ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboNonUniquePredicateBuilder(); 1583 Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString); 1584 mySqlBuilder.addPredicate(predicate); 1585 } 1586 1587 1588 // expand out the pids 1589 public void addPredicateEverythingOperation(String theResourceName, Long... theTargetPids) { 1590 ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null); 1591 Condition predicate = table.createEverythingPredicate(theResourceName, theTargetPids); 1592 mySqlBuilder.addPredicate(predicate); 1593 } 1594 1595 private IQueryParameterType toParameterType(RuntimeSearchParam theParam) { 1596 1597 IQueryParameterType qp; 1598 switch (theParam.getParamType()) { 1599 case DATE: 1600 qp = new DateParam(); 1601 break; 1602 case NUMBER: 1603 qp = new NumberParam(); 1604 break; 1605 case QUANTITY: 1606 qp = new QuantityParam(); 1607 break; 1608 case STRING: 1609 qp = new StringParam(); 1610 break; 1611 case TOKEN: 1612 qp = new TokenParam(); 1613 break; 1614 case COMPOSITE: 1615 List<RuntimeSearchParam> compositeOf = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam); 1616 if (compositeOf.size() != 2) { 1617 throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this."); 1618 } 1619 IQueryParameterType leftParam = toParameterType(compositeOf.get(0)); 1620 IQueryParameterType rightParam = toParameterType(compositeOf.get(1)); 1621 qp = new CompositeParam<>(leftParam, rightParam); 1622 break; 1623 case URI: 1624 qp = new UriParam(); 1625 break; 1626 case HAS: 1627 case REFERENCE: 1628 case SPECIAL: 1629 default: 1630 throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported."); 1631 } 1632 return qp; 1633 } 1634 1635 private enum PredicateBuilderTypeEnum { 1636 DATE, COORDS, NUMBER, QUANTITY, REFERENCE, SOURCE, STRING, TOKEN, TAG 1637 } 1638 1639 private static class PredicateBuilderCacheLookupResult<T extends BaseJoiningPredicateBuilder> { 1640 private final boolean myCacheHit; 1641 private final T myResult; 1642 1643 private PredicateBuilderCacheLookupResult(boolean theCacheHit, T theResult) { 1644 myCacheHit = theCacheHit; 1645 myResult = theResult; 1646 } 1647 1648 public boolean isCacheHit() { 1649 return myCacheHit; 1650 } 1651 1652 public T getResult() { 1653 return myResult; 1654 } 1655 } 1656 1657 private static class PredicateBuilderCacheKey { 1658 private final DbColumn myDbColumn; 1659 private final PredicateBuilderTypeEnum myType; 1660 private final String myParamName; 1661 private final int myHashCode; 1662 1663 private PredicateBuilderCacheKey(DbColumn theDbColumn, PredicateBuilderTypeEnum theType, String theParamName) { 1664 myDbColumn = theDbColumn; 1665 myType = theType; 1666 myParamName = theParamName; 1667 myHashCode = new HashCodeBuilder().append(myDbColumn).append(myType).append(myParamName).toHashCode(); 1668 } 1669 1670 @Override 1671 public boolean equals(Object theO) { 1672 if (this == theO) { 1673 return true; 1674 } 1675 1676 if (theO == null || getClass() != theO.getClass()) { 1677 return false; 1678 } 1679 1680 PredicateBuilderCacheKey that = (PredicateBuilderCacheKey) theO; 1681 1682 return new EqualsBuilder() 1683 .append(myDbColumn, that.myDbColumn) 1684 .append(myType, that.myType) 1685 .append(myParamName, that.myParamName) 1686 .isEquals(); 1687 } 1688 1689 @Override 1690 public int hashCode() { 1691 return myHashCode; 1692 } 1693 } 1694 1695 @Nullable 1696 public static Condition toAndPredicate(List<Condition> theAndPredicates) { 1697 List<Condition> andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList()); 1698 if (andPredicates.size() == 0) { 1699 return null; 1700 } else if (andPredicates.size() == 1) { 1701 return andPredicates.get(0); 1702 } else { 1703 return ComboCondition.and(andPredicates.toArray(new Condition[0])); 1704 } 1705 } 1706 1707 @Nullable 1708 public static Condition toOrPredicate(List<Condition> theOrPredicates) { 1709 List<Condition> orPredicates = theOrPredicates.stream().filter(t -> t != null).collect(Collectors.toList()); 1710 if (orPredicates.size() == 0) { 1711 return null; 1712 } else if (orPredicates.size() == 1) { 1713 return orPredicates.get(0); 1714 } else { 1715 return ComboCondition.or(orPredicates.toArray(new Condition[0])); 1716 } 1717 } 1718 1719 @Nullable 1720 public static Condition toOrPredicate(Condition... theOrPredicates) { 1721 return toOrPredicate(Arrays.asList(theOrPredicates)); 1722 } 1723 1724 @Nullable 1725 public static Condition toAndPredicate(Condition... theAndPredicates) { 1726 return toAndPredicate(Arrays.asList(theAndPredicates)); 1727 } 1728 1729 @Nonnull 1730 public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders, boolean theInverse) { 1731 if (theInverse) { 1732 return toNotEqualToOrNotInPredicate(theColumn, theValuePlaceholders); 1733 } else { 1734 return toEqualToOrInPredicate(theColumn, theValuePlaceholders); 1735 } 1736 } 1737 1738 @Nonnull 1739 public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) { 1740 if (theValuePlaceholders.size() == 1) { 1741 return BinaryCondition.equalTo(theColumn, theValuePlaceholders.get(0)); 1742 } 1743 return new InCondition(theColumn, theValuePlaceholders); 1744 } 1745 1746 @Nonnull 1747 public static Condition toNotEqualToOrNotInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) { 1748 if (theValuePlaceholders.size() == 1) { 1749 return BinaryCondition.notEqualTo(theColumn, theValuePlaceholders.get(0)); 1750 } 1751 return new InCondition(theColumn, theValuePlaceholders).setNegate(true); 1752 } 1753 1754 public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) { 1755 SearchFilterParser.CompareOperation retVal = null; 1756 if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) { 1757 retVal = ourCompareOperationToParamPrefix.getKey(thePrefix); 1758 } 1759 return defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq); 1760 } 1761 1762 public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) { 1763 ParamPrefixEnum retVal = null; 1764 if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) { 1765 retVal = ourCompareOperationToParamPrefix.get(thePrefix); 1766 } 1767 return defaultIfNull(retVal, ParamPrefixEnum.EQUAL); 1768 } 1769 1770 private static String getChainedPart(String parameter) { 1771 return parameter.substring(parameter.indexOf(".") + 1); 1772 } 1773 1774 public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) { 1775 1776 if (isBlank(theSpnamePrefix)) 1777 return theParamName; 1778 1779 return theSpnamePrefix + "." + theParamName; 1780 } 1781}