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}