
001package ca.uhn.fhir.jpa.dao.predicate; 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.i18n.Msg; 024import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 025import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition; 026import ca.uhn.fhir.context.RuntimeSearchParam; 027import ca.uhn.fhir.context.support.ValueSetExpansionOptions; 028import ca.uhn.fhir.interceptor.model.RequestPartitionId; 029import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; 030import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 031import ca.uhn.fhir.jpa.model.entity.ModelConfig; 032import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 033import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; 034import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 035import ca.uhn.fhir.model.api.IQueryParameterType; 036import ca.uhn.fhir.model.base.composite.BaseCodingDt; 037import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; 038import ca.uhn.fhir.rest.param.NumberParam; 039import ca.uhn.fhir.rest.param.TokenParam; 040import ca.uhn.fhir.rest.param.TokenParamModifier; 041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 042import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 043import ca.uhn.fhir.util.FhirVersionIndependentConcept; 044import com.google.common.collect.Sets; 045import org.hibernate.query.criteria.internal.CriteriaBuilderImpl; 046import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate; 047import org.springframework.beans.factory.annotation.Autowired; 048import org.springframework.context.annotation.Scope; 049import org.springframework.stereotype.Component; 050 051import javax.persistence.criteria.CriteriaBuilder; 052import javax.persistence.criteria.Expression; 053import javax.persistence.criteria.From; 054import javax.persistence.criteria.Path; 055import javax.persistence.criteria.Predicate; 056import java.util.ArrayList; 057import java.util.Collection; 058import java.util.Collections; 059import java.util.List; 060import java.util.Set; 061import java.util.stream.Collectors; 062 063import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 064import static org.apache.commons.lang3.StringUtils.isBlank; 065import static org.apache.commons.lang3.StringUtils.isNotBlank; 066 067@Component 068@Scope("prototype") 069public 070class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBuilder { 071 private final PredicateBuilder myPredicateBuilder; 072 @Autowired 073 private ITermReadSvc myTerminologySvc; 074 @Autowired 075 private ModelConfig myModelConfig; 076 077 public PredicateBuilderToken(LegacySearchBuilder theSearchBuilder, PredicateBuilder thePredicateBuilder) { 078 super(theSearchBuilder); 079 myPredicateBuilder = thePredicateBuilder; 080 } 081 082 @Override 083 public Predicate addPredicate(String theResourceName, 084 RuntimeSearchParam theSearchParam, 085 List<? extends IQueryParameterType> theList, 086 SearchFilterParser.CompareOperation theOperation, 087 RequestPartitionId theRequestPartitionId) { 088 089 if (theList.get(0).getMissing() != null) { 090 From<?, ResourceIndexedSearchParamToken> join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theSearchParam.getName()); 091 addPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), join, theRequestPartitionId); 092 return null; 093 } 094 095 List<Predicate> codePredicates = new ArrayList<>(); 096 097 List<IQueryParameterType> tokens = new ArrayList<>(); 098 for (IQueryParameterType nextOr : theList) { 099 100 if (nextOr instanceof TokenParam) { 101 TokenParam id = (TokenParam) nextOr; 102 if (id.isText()) { 103 104 // Check whether the :text modifier is actually enabled here 105 boolean tokenTextIndexingEnabled = BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam); 106 if (!tokenTextIndexingEnabled) { 107 String msg; 108 if (myModelConfig.isSuppressStringIndexingInTokens()) { 109 msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForServer"); 110 } else { 111 msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForSearchParam"); 112 } 113 throw new MethodNotAllowedException(Msg.code(1032) + msg); 114 } 115 116 myPredicateBuilder.addPredicateString(theResourceName, theSearchParam, theList, theOperation, theRequestPartitionId); 117 break; 118 } 119 } 120 121 tokens.add(nextOr); 122 } 123 124 if (tokens.isEmpty()) { 125 return null; 126 } 127 128 From<?, ResourceIndexedSearchParamToken> join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theSearchParam.getName()); 129 addPartitionIdPredicate(theRequestPartitionId, join, codePredicates); 130 131 Collection<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theSearchParam, myCriteriaBuilder, join, theOperation, theRequestPartitionId); 132 assert singleCode != null; 133 codePredicates.addAll(singleCode); 134 135 Predicate spPredicate = myCriteriaBuilder.or(toArray(codePredicates)); 136 137 myQueryStack.addPredicateWithImplicitTypeSelection(spPredicate); 138 139 return spPredicate; 140 } 141 142 public Collection<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters, 143 String theResourceName, 144 RuntimeSearchParam theSearchParam, 145 CriteriaBuilder theBuilder, 146 From<?, ResourceIndexedSearchParamToken> theFrom, 147 RequestPartitionId theRequestPartitionId) { 148 return createPredicateToken( 149 theParameters, 150 theResourceName, 151 theSearchParam, 152 theBuilder, 153 theFrom, 154 null, 155 theRequestPartitionId); 156 } 157 158 private Collection<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters, 159 String theResourceName, 160 RuntimeSearchParam theSearchParam, 161 CriteriaBuilder theBuilder, 162 From<?, ResourceIndexedSearchParamToken> theFrom, 163 SearchFilterParser.CompareOperation operation, 164 RequestPartitionId theRequestPartitionId) { 165 final List<FhirVersionIndependentConcept> codes = new ArrayList<>(); 166 String paramName = theSearchParam.getName(); 167 168 TokenParamModifier modifier = null; 169 for (IQueryParameterType nextParameter : theParameters) { 170 171 String code; 172 String system; 173 if (nextParameter instanceof TokenParam) { 174 TokenParam id = (TokenParam) nextParameter; 175 system = id.getSystem(); 176 code = (id.getValue()); 177 modifier = id.getModifier(); 178 } else if (nextParameter instanceof BaseIdentifierDt) { 179 BaseIdentifierDt id = (BaseIdentifierDt) nextParameter; 180 system = id.getSystemElement().getValueAsString(); 181 code = (id.getValueElement().getValue()); 182 } else if (nextParameter instanceof BaseCodingDt) { 183 BaseCodingDt id = (BaseCodingDt) nextParameter; 184 system = id.getSystemElement().getValueAsString(); 185 code = (id.getCodeElement().getValue()); 186 } else if (nextParameter instanceof NumberParam) { 187 NumberParam number = (NumberParam) nextParameter; 188 system = null; 189 code = number.getValueAsQueryToken(myContext); 190 } else { 191 throw new IllegalArgumentException(Msg.code(1033) + "Invalid token type: " + nextParameter.getClass()); 192 } 193 194 if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { 195 throw new InvalidRequestException(Msg.code(1034) + "Parameter[" + paramName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); 196 } 197 198 if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { 199 throw new InvalidRequestException(Msg.code(1035) + "Parameter[" + paramName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); 200 } 201 202 /* 203 * Process token modifiers (:in, :below, :above) 204 */ 205 206 if (modifier == TokenParamModifier.IN) { 207 codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code)); 208 } else if (modifier == TokenParamModifier.ABOVE) { 209 system = determineSystemIfMissing(theSearchParam, code, system); 210 validateHaveSystemAndCodeForToken(paramName, code, system); 211 codes.addAll(myTerminologySvc.findCodesAbove(system, code)); 212 } else if (modifier == TokenParamModifier.BELOW) { 213 system = determineSystemIfMissing(theSearchParam, code, system); 214 validateHaveSystemAndCodeForToken(paramName, code, system); 215 codes.addAll(myTerminologySvc.findCodesBelow(system, code)); 216 } else { 217 codes.add(new FhirVersionIndependentConcept(system, code)); 218 } 219 220 } 221 222 List<FhirVersionIndependentConcept> sortedCodesList = codes 223 .stream() 224 .filter(t -> t.getCode() != null || t.getSystem() != null) 225 .sorted() 226 .distinct() 227 .collect(Collectors.toList()); 228 229 if (codes.isEmpty()) { 230 // This will never match anything 231 return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false)); 232 } 233 234 List<Predicate> retVal = new ArrayList<>(); 235 236 // System only 237 List<FhirVersionIndependentConcept> systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList()); 238 if (!systemOnlyCodes.isEmpty()) { 239 retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, systemOnlyCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_ONLY, theRequestPartitionId)); 240 } 241 242 // Code only 243 List<FhirVersionIndependentConcept> codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList()); 244 if (!codeOnlyCodes.isEmpty()) { 245 retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, codeOnlyCodes, modifier, SearchBuilderTokenModeEnum.VALUE_ONLY, theRequestPartitionId)); 246 } 247 248 // System and code 249 List<FhirVersionIndependentConcept> systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList()); 250 if (!systemAndCodeCodes.isEmpty()) { 251 retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, systemAndCodeCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_AND_VALUE, theRequestPartitionId)); 252 } 253 254 return retVal; 255 } 256 257 private String determineSystemIfMissing(RuntimeSearchParam theSearchParam, String code, String theSystem) { 258 String retVal = theSystem; 259 if (retVal == null) { 260 if (theSearchParam != null) { 261 Set<String> valueSetUris = Sets.newHashSet(); 262 for (String nextPath : theSearchParam.getPathsSplit()) { 263 if (!nextPath.startsWith(myResourceType + ".")) { 264 continue; 265 } 266 BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath); 267 if (def instanceof BaseRuntimeDeclaredChildDefinition) { 268 String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet(); 269 if (isNotBlank(valueSet)) { 270 valueSetUris.add(valueSet); 271 } 272 } 273 } 274 if (valueSetUris.size() == 1) { 275 String valueSet = valueSetUris.iterator().next(); 276 ValueSetExpansionOptions options = new ValueSetExpansionOptions() 277 .setFailOnMissingCodeSystem(false); 278 List<FhirVersionIndependentConcept> candidateCodes = myTerminologySvc.expandValueSetIntoConceptList(options, valueSet); 279 for (FhirVersionIndependentConcept nextCandidate : candidateCodes) { 280 if (nextCandidate.getCode().equals(code)) { 281 retVal = nextCandidate.getSystem(); 282 break; 283 } 284 } 285 } 286 } 287 } 288 return retVal; 289 } 290 291 private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) { 292 String systemDesc = defaultIfBlank(theSystem, "(missing)"); 293 String codeDesc = defaultIfBlank(theCode, "(missing)"); 294 if (isBlank(theCode)) { 295 String msg = myContext.getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc); 296 throw new InvalidRequestException(Msg.code(1036) + msg); 297 } 298 if (isBlank(theSystem)) { 299 String msg = myContext.getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc); 300 throw new InvalidRequestException(Msg.code(1037) + msg); 301 } 302 } 303 304 private Predicate addPredicate(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<FhirVersionIndependentConcept> theTokens, TokenParamModifier theModifier, SearchBuilderTokenModeEnum theTokenMode, RequestPartitionId theRequestPartitionId) { 305 if (myDontUseHashesForSearch) { 306 final Path<String> systemExpression = theFrom.get("mySystem"); 307 final Path<String> valueExpression = theFrom.get("myValue"); 308 309 List<Predicate> orPredicates = new ArrayList<>(); 310 switch (theTokenMode) { 311 case SYSTEM_ONLY: { 312 List<String> systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList()); 313 Predicate orPredicate = systemExpression.in(systems); 314 orPredicates.add(orPredicate); 315 break; 316 } 317 case VALUE_ONLY: 318 List<String> codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList()); 319 Predicate orPredicate = valueExpression.in(codes); 320 orPredicates.add(orPredicate); 321 break; 322 case SYSTEM_AND_VALUE: 323 for (FhirVersionIndependentConcept next : theTokens) { 324 orPredicates.add(theBuilder.and( 325 toEqualOrIsNullPredicate(systemExpression, next.getSystem()), 326 toEqualOrIsNullPredicate(valueExpression, next.getCode()) 327 )); 328 } 329 break; 330 } 331 332 Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0])); 333 if (theModifier == TokenParamModifier.NOT) { 334 or = theBuilder.not(or); 335 } 336 337 return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or, theRequestPartitionId); 338 } 339 340 /* 341 * Note: A null system value means "match any system", but 342 * an empty-string system value means "match values that 343 * explicitly have no system". 344 */ 345 Expression<Long> hashField; 346 List<Long> values; 347 switch (theTokenMode) { 348 case SYSTEM_ONLY: 349 hashField = theFrom.get("myHashSystem").as(Long.class); 350 values = theTokens 351 .stream() 352 .map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getSystem())) 353 .collect(Collectors.toList()); 354 break; 355 case VALUE_ONLY: 356 hashField = theFrom.get("myHashValue").as(Long.class); 357 values = theTokens 358 .stream() 359 .map(t -> ResourceIndexedSearchParamToken.calculateHashValue(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getCode())) 360 .collect(Collectors.toList()); 361 break; 362 case SYSTEM_AND_VALUE: 363 default: 364 hashField = theFrom.get("myHashSystemAndValue").as(Long.class); 365 values = theTokens 366 .stream() 367 .map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getSystem(), t.getCode())) 368 .collect(Collectors.toList()); 369 break; 370 } 371 372 /* 373 * Note: At one point we had an IF-ELSE here that did an equals if there was only 1 value, and an IN if there 374 * was more than 1. This caused a performance regression for some reason in Postgres though. So maybe simpler 375 * is better.. 376 */ 377 Predicate predicate = hashField.in(values); 378 379 if (theModifier == TokenParamModifier.NOT) { 380 Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName)); 381 Predicate disjunctionPredicate = theBuilder.not(predicate); 382 predicate = theBuilder.and(identityPredicate, disjunctionPredicate); 383 } 384 return predicate; 385 } 386 387 private <T> Expression<Boolean> toEqualOrIsNullPredicate(Path<T> theExpression, T theCode) { 388 if (theCode == null) { 389 return myCriteriaBuilder.isNull(theExpression); 390 } 391 return myCriteriaBuilder.equal(theExpression, theCode); 392 } 393}