001package ca.uhn.fhir.jpa.search.builder.predicate;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 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.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.jpa.dao.LegacySearchBuilder;
026import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
027import ca.uhn.fhir.jpa.dao.predicate.SearchFuzzUtil;
028import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
029import ca.uhn.fhir.model.api.IQueryParameterType;
030import ca.uhn.fhir.rest.param.ParamPrefixEnum;
031import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
032import com.healthmarketscience.sqlbuilder.BinaryCondition;
033import com.healthmarketscience.sqlbuilder.ComboCondition;
034import com.healthmarketscience.sqlbuilder.Condition;
035import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038import org.springframework.beans.factory.annotation.Autowired;
039
040import java.math.BigDecimal;
041import java.math.MathContext;
042
043import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
044
045public class NumberPredicateBuilder extends BaseSearchParamPredicateBuilder {
046
047        private static final Logger ourLog = LoggerFactory.getLogger(NumberPredicateBuilder.class);
048        private final DbColumn myColumnValue;
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(String theResourceName, String theParamName, SearchFilterParser.CompareOperation theOperation, BigDecimal theValue, RequestPartitionId theRequestPartitionId, IQueryParameterType theActualParam) {
062                Condition numericPredicate = createPredicateNumeric(this, theOperation, theValue, myColumnValue, "invalidNumberPrefix", myFhirContext, theActualParam);
063                return combineWithHashIdentityPredicate(theResourceName, theParamName, numericPredicate);
064        }
065
066        public DbColumn getColumnValue() {
067                return myColumnValue;
068        }
069
070
071        static Condition createPredicateNumeric(BaseSearchParamPredicateBuilder theIndexTable, SearchFilterParser.CompareOperation theOperation, BigDecimal theValue, DbColumn theColumn, String theInvalidValueKey, FhirContext theFhirContext, IQueryParameterType theActualParam) {
072                Condition num;
073
074                // Per discussions with Grahame Grieve and James Agnew on 11/13/19, modified logic for EQUAL and NOT_EQUAL operators below so as to
075                //   use exact value matching.  The "fuzz amount" matching is still used with the APPROXIMATE operator.
076                SearchFilterParser.CompareOperation operation = defaultIfNull(theOperation, SearchFilterParser.CompareOperation.eq);
077                switch (operation) {
078                        case gt:
079                                num = BinaryCondition.greaterThan(theColumn, theIndexTable.generatePlaceholder(theValue));
080                                break;
081                        case ge:
082                                num = BinaryCondition.greaterThanOrEq(theColumn, theIndexTable.generatePlaceholder(theValue));
083                                break;
084                        case lt:
085                                num = BinaryCondition.lessThan(theColumn, theIndexTable.generatePlaceholder(theValue));
086                                break;
087                        case le:
088                                num = BinaryCondition.lessThanOrEq(theColumn, theIndexTable.generatePlaceholder(theValue));
089                                break;
090                        case eq:
091                                num = BinaryCondition.equalTo(theColumn, theIndexTable.generatePlaceholder(theValue));
092                                break;
093                        case ne:
094                                num = BinaryCondition.notEqualTo(theColumn, theIndexTable.generatePlaceholder(theValue));
095                                break;
096                        case ap:
097                                BigDecimal mul = SearchFuzzUtil.calculateFuzzAmount(ParamPrefixEnum.APPROXIMATE, theValue);
098                                BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64);
099                                BigDecimal high = theValue.add(mul, MathContext.DECIMAL64);
100                                Condition lowPred = BinaryCondition.greaterThanOrEq(theColumn, theIndexTable.generatePlaceholder(low));
101                                Condition highPred = BinaryCondition.lessThanOrEq(theColumn, theIndexTable.generatePlaceholder(high));
102                                num = ComboCondition.and(lowPred, highPred);
103                                ourLog.trace("Searching for {} <= val <= {}", low, high);
104                                break;
105                        default:
106                                String paramValue = theActualParam.getValueAsQueryToken(theFhirContext);
107                                String msg = theIndexTable.getFhirContext().getLocalizer().getMessage(LegacySearchBuilder.class, theInvalidValueKey, operation, paramValue);
108                                throw new InvalidRequestException(msg);
109                }
110
111                return num;
112        }
113}