001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.builder.predicate; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 026import ca.uhn.fhir.jpa.dao.predicate.SearchFuzzUtil; 027import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 028import ca.uhn.fhir.model.api.IQueryParameterType; 029import ca.uhn.fhir.rest.param.ParamPrefixEnum; 030import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 031import com.healthmarketscience.sqlbuilder.BinaryCondition; 032import com.healthmarketscience.sqlbuilder.ComboCondition; 033import com.healthmarketscience.sqlbuilder.Condition; 034import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.springframework.beans.factory.annotation.Autowired; 038 039import java.math.BigDecimal; 040import java.math.MathContext; 041 042import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 043 044public class NumberPredicateBuilder extends BaseSearchParamPredicateBuilder { 045 046 private static final Logger ourLog = LoggerFactory.getLogger(NumberPredicateBuilder.class); 047 private final DbColumn myColumnValue; 048 049 @Autowired 050 private FhirContext myFhirContext; 051 052 /** 053 * Constructor 054 */ 055 public NumberPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) { 056 super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_NUMBER")); 057 058 myColumnValue = getTable().addColumn("SP_VALUE"); 059 } 060 061 public Condition createPredicateNumeric( 062 String theResourceName, 063 String theParamName, 064 SearchFilterParser.CompareOperation theOperation, 065 BigDecimal theValue, 066 RequestPartitionId theRequestPartitionId, 067 IQueryParameterType theActualParam) { 068 Condition numericPredicate = createPredicateNumeric( 069 this, theOperation, theValue, myColumnValue, "invalidNumberPrefix", myFhirContext, theActualParam); 070 return combineWithHashIdentityPredicate(theResourceName, theParamName, numericPredicate); 071 } 072 073 public DbColumn getColumnValue() { 074 return myColumnValue; 075 } 076 077 static Condition createPredicateNumeric( 078 BaseSearchParamPredicateBuilder theIndexTable, 079 SearchFilterParser.CompareOperation theOperation, 080 BigDecimal theValue, 081 DbColumn theColumn, 082 String theInvalidValueKey, 083 FhirContext theFhirContext, 084 IQueryParameterType theActualParam) { 085 Condition num; 086 087 // Per discussions with Grahame Grieve and James Agnew on 11/13/19, modified logic for EQUAL and NOT_EQUAL 088 // operators below so as to 089 // use exact value matching. The "fuzz amount" matching is still used with the APPROXIMATE operator. 090 SearchFilterParser.CompareOperation operation = 091 defaultIfNull(theOperation, SearchFilterParser.CompareOperation.eq); 092 switch (operation) { 093 case gt: 094 num = BinaryCondition.greaterThan(theColumn, theIndexTable.generatePlaceholder(theValue)); 095 break; 096 case ge: 097 num = BinaryCondition.greaterThanOrEq(theColumn, theIndexTable.generatePlaceholder(theValue)); 098 break; 099 case lt: 100 num = BinaryCondition.lessThan(theColumn, theIndexTable.generatePlaceholder(theValue)); 101 break; 102 case le: 103 num = BinaryCondition.lessThanOrEq(theColumn, theIndexTable.generatePlaceholder(theValue)); 104 break; 105 case eq: 106 num = BinaryCondition.equalTo(theColumn, theIndexTable.generatePlaceholder(theValue)); 107 break; 108 case ne: 109 num = BinaryCondition.notEqualTo(theColumn, theIndexTable.generatePlaceholder(theValue)); 110 break; 111 case ap: 112 BigDecimal mul = SearchFuzzUtil.calculateFuzzAmount(ParamPrefixEnum.APPROXIMATE, theValue); 113 BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64); 114 BigDecimal high = theValue.add(mul, MathContext.DECIMAL64); 115 Condition lowPred = BinaryCondition.greaterThanOrEq(theColumn, theIndexTable.generatePlaceholder(low)); 116 Condition highPred = BinaryCondition.lessThanOrEq(theColumn, theIndexTable.generatePlaceholder(high)); 117 num = ComboCondition.and(lowPred, highPred); 118 ourLog.trace("Searching for {} <= val <= {}", low, high); 119 break; 120 default: 121 String paramValue = theActualParam.getValueAsQueryToken(theFhirContext); 122 String msg = theIndexTable 123 .getFhirContext() 124 .getLocalizer() 125 .getMessage(NumberPredicateBuilder.class, theInvalidValueKey, operation, paramValue); 126 throw new InvalidRequestException(Msg.code(1235) + msg); 127 } 128 129 return num; 130 } 131}