001package ca.uhn.fhir.jpa.search.builder.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.RuntimeSearchParam;
025import ca.uhn.fhir.jpa.api.config.DaoConfig;
026import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
027import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
028import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
029import ca.uhn.fhir.jpa.search.builder.QueryStack;
030import ca.uhn.fhir.model.api.IPrimitiveDatatype;
031import ca.uhn.fhir.model.api.IQueryParameterType;
032import ca.uhn.fhir.rest.param.StringParam;
033import ca.uhn.fhir.rest.param.TokenParam;
034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
035import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
036import ca.uhn.fhir.util.StringUtil;
037import com.healthmarketscience.sqlbuilder.BinaryCondition;
038import com.healthmarketscience.sqlbuilder.ComboCondition;
039import com.healthmarketscience.sqlbuilder.Condition;
040import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
041import org.springframework.beans.factory.annotation.Autowired;
042
043import javax.annotation.Nonnull;
044
045public class StringPredicateBuilder extends BaseSearchParamPredicateBuilder {
046
047        private final DbColumn myColumnResId;
048        private final DbColumn myColumnValueExact;
049        private final DbColumn myColumnValueNormalized;
050        private final DbColumn myColumnHashNormPrefix;
051        private final DbColumn myColumnHashIdentity;
052        private final DbColumn myColumnHashExact;
053        @Autowired
054        private DaoConfig myDaoConfig;
055
056        /**
057         * Constructor
058         */
059        public StringPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
060                super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_STRING"));
061                myColumnResId = getTable().addColumn("RES_ID");
062                myColumnValueExact = getTable().addColumn("SP_VALUE_EXACT");
063                myColumnValueNormalized = getTable().addColumn("SP_VALUE_NORMALIZED");
064                myColumnHashNormPrefix = getTable().addColumn("HASH_NORM_PREFIX");
065                myColumnHashIdentity = getTable().addColumn("HASH_IDENTITY");
066                myColumnHashExact = getTable().addColumn("HASH_EXACT");
067        }
068
069        public DbColumn getColumnValueNormalized() {
070                return myColumnValueNormalized;
071        }
072
073        @Override
074        public DbColumn getResourceIdColumn() {
075                return myColumnResId;
076        }
077
078        public Condition createPredicateString(IQueryParameterType theParameter,
079                                                                                                                String theResourceName,
080                                                                                                                String theSpnamePrefix,
081                                                                                                                RuntimeSearchParam theSearchParam,
082                                                                                                                StringPredicateBuilder theFrom,
083                                                                                                                SearchFilterParser.CompareOperation operation) {
084                String rawSearchTerm;
085                String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
086                
087                if (theParameter instanceof TokenParam) {
088                        TokenParam id = (TokenParam) theParameter;
089                        if (!id.isText()) {
090                                throw new IllegalStateException(Msg.code(1257) + "Trying to process a text search on a non-text token parameter");
091                        }
092                        rawSearchTerm = id.getValue();
093                } else if (theParameter instanceof StringParam) {
094                        StringParam id = (StringParam) theParameter;
095                        rawSearchTerm = id.getValue();
096                        if (id.isContains()) {
097                                if (!myDaoConfig.isAllowContainsSearches()) {
098                                        throw new MethodNotAllowedException(Msg.code(1258) + ":contains modifier is disabled on this server");
099                                }
100                        } else {
101                                rawSearchTerm = theSearchParam.encode(rawSearchTerm);
102                        }
103                } else if (theParameter instanceof IPrimitiveDatatype<?>) {
104                        IPrimitiveDatatype<?> id = (IPrimitiveDatatype<?>) theParameter;
105                        rawSearchTerm = id.getValueAsString();
106                } else {
107                        throw new IllegalArgumentException(Msg.code(1259) + "Invalid token type: " + theParameter.getClass());
108                }
109
110                if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
111                        throw new InvalidRequestException(Msg.code(1260) + "Parameter[" + paramName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed ("
112                                + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm);
113                }
114
115                boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact();
116                if (exactMatch) {
117                        // Exact match
118                        return theFrom.createPredicateExact(theResourceName, paramName, rawSearchTerm);
119                } else {
120                        // Normalized Match
121                        String normalizedString = StringUtil.normalizeStringForSearchIndexing(rawSearchTerm);
122                        String likeExpression;
123                        if ((theParameter instanceof StringParam) &&
124                                (((((StringParam) theParameter).isContains()) &&
125                                        (myDaoConfig.isAllowContainsSearches())) ||
126                                        (operation == SearchFilterParser.CompareOperation.co))) {
127                                likeExpression = createLeftAndRightMatchLikeExpression(normalizedString);
128                        } else if ((operation != SearchFilterParser.CompareOperation.ne) &&
129                                (operation != SearchFilterParser.CompareOperation.gt) &&
130                                (operation != SearchFilterParser.CompareOperation.lt) &&
131                                (operation != SearchFilterParser.CompareOperation.ge) &&
132                                (operation != SearchFilterParser.CompareOperation.le)) {
133                                if (operation == SearchFilterParser.CompareOperation.ew) {
134                                        likeExpression = createRightMatchLikeExpression(normalizedString);
135                                } else {
136                                        likeExpression = createLeftMatchLikeExpression(normalizedString);
137                                }
138                        } else {
139                                likeExpression = normalizedString;
140                        }
141
142                        Condition predicate;
143                        if ((operation == null) ||
144                                (operation == SearchFilterParser.CompareOperation.sw)) {
145                                predicate = theFrom.createPredicateNormalLike(theResourceName, paramName, normalizedString, likeExpression);
146                        } else if ((operation == SearchFilterParser.CompareOperation.ew) || (operation == SearchFilterParser.CompareOperation.co)) {
147                                predicate = theFrom.createPredicateLikeExpressionOnly(theResourceName, paramName, likeExpression, false);
148                        } else if (operation == SearchFilterParser.CompareOperation.eq) {
149                                predicate = theFrom.createPredicateNormal(theResourceName, paramName, normalizedString);
150                        } else if (operation == SearchFilterParser.CompareOperation.ne) {
151                                predicate = theFrom.createPredicateLikeExpressionOnly(theResourceName, paramName, likeExpression, true);
152                        } else if (operation == SearchFilterParser.CompareOperation.gt) {
153                                predicate = theFrom.createPredicateNormalGreaterThan(theResourceName, paramName, likeExpression);
154                        } else if (operation == SearchFilterParser.CompareOperation.ge) {
155                                predicate = theFrom.createPredicateNormalGreaterThanOrEqual(theResourceName, paramName, likeExpression);
156                        } else if (operation == SearchFilterParser.CompareOperation.lt) {
157                                predicate = theFrom.createPredicateNormalLessThan(theResourceName, paramName, likeExpression);
158                        } else if (operation == SearchFilterParser.CompareOperation.le) {
159                                predicate = theFrom.createPredicateNormalLessThanOrEqual(theResourceName, paramName, likeExpression);
160                        } else {
161                                throw new IllegalArgumentException(Msg.code(1261) + "Don't yet know how to handle operation " + operation + " on a string");
162                        }
163
164                        return predicate;
165                }
166        }
167
168        @Nonnull
169        public Condition createPredicateExact(String theResourceType, String theParamName, String theTheValueExact) {
170                long hash = ResourceIndexedSearchParamString.calculateHashExact(getPartitionSettings(), getRequestPartitionId(), theResourceType, theParamName, theTheValueExact);
171                String placeholderValue = generatePlaceholder(hash);
172                return BinaryCondition.equalTo(myColumnHashExact, placeholderValue);
173        }
174
175        @Nonnull
176        public Condition createPredicateNormalLike(String theResourceType, String theParamName, String theNormalizedString, String theLikeExpression) {
177                Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(getPartitionSettings(), getRequestPartitionId(), getModelConfig(), theResourceType, theParamName, theNormalizedString);
178                Condition hashPredicate = BinaryCondition.equalTo(myColumnHashNormPrefix, generatePlaceholder(hash));
179                Condition valuePredicate = BinaryCondition.like(myColumnValueNormalized, generatePlaceholder(theLikeExpression));
180                return ComboCondition.and(hashPredicate, valuePredicate);
181        }
182
183        @Nonnull
184        public Condition createPredicateNormal(String theResourceType, String theParamName, String theNormalizedString) {
185                Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(getPartitionSettings(), getRequestPartitionId(), getModelConfig(), theResourceType, theParamName, theNormalizedString);
186                Condition hashPredicate = BinaryCondition.equalTo(myColumnHashNormPrefix, generatePlaceholder(hash));
187                Condition valuePredicate = BinaryCondition.equalTo(myColumnValueNormalized, generatePlaceholder(theNormalizedString));
188                return ComboCondition.and(hashPredicate, valuePredicate);
189        }
190
191        private Condition createPredicateNormalGreaterThanOrEqual(String theResourceType, String theParamName, String theNormalizedString) {
192                Condition hashPredicate = createHashIdentityPredicate(theResourceType, theParamName);
193                Condition valuePredicate = BinaryCondition.greaterThanOrEq(myColumnValueNormalized, generatePlaceholder(theNormalizedString));
194                return ComboCondition.and(hashPredicate, valuePredicate);
195        }
196
197        private Condition createPredicateNormalGreaterThan(String theResourceType, String theParamName, String theNormalizedString) {
198                Condition hashPredicate = createHashIdentityPredicate(theResourceType, theParamName);
199                Condition valuePredicate = BinaryCondition.greaterThan(myColumnValueNormalized, generatePlaceholder(theNormalizedString));
200                return ComboCondition.and(hashPredicate, valuePredicate);
201        }
202
203        private Condition createPredicateNormalLessThanOrEqual(String theResourceType, String theParamName, String theNormalizedString) {
204                Condition hashPredicate = createHashIdentityPredicate(theResourceType, theParamName);
205                Condition valuePredicate = BinaryCondition.lessThanOrEq(myColumnValueNormalized, generatePlaceholder(theNormalizedString));
206                return ComboCondition.and(hashPredicate, valuePredicate);
207        }
208
209        private Condition createPredicateNormalLessThan(String theResourceType, String theParamName, String theNormalizedString) {
210                Condition hashPredicate = createHashIdentityPredicate(theResourceType, theParamName);
211                Condition valuePredicate = BinaryCondition.lessThan(myColumnValueNormalized, generatePlaceholder(theNormalizedString));
212                return ComboCondition.and(hashPredicate, valuePredicate);
213        }
214
215        @Nonnull
216        public Condition createPredicateLikeExpressionOnly(String theResourceType, String theParamName, String theLikeExpression, boolean theInverse) {
217                long hashIdentity = ResourceIndexedSearchParamString.calculateHashIdentity(getPartitionSettings(), getRequestPartitionId(), theResourceType, theParamName);
218                BinaryCondition identityPredicate = BinaryCondition.equalTo(myColumnHashIdentity, generatePlaceholder(hashIdentity));
219                BinaryCondition likePredicate;
220                if (theInverse) {
221                        likePredicate = BinaryCondition.notLike(myColumnValueNormalized, generatePlaceholder(theLikeExpression));
222                } else {
223                        likePredicate = BinaryCondition.like(myColumnValueNormalized, generatePlaceholder(theLikeExpression));
224                }
225                return ComboCondition.and(identityPredicate, likePredicate);
226        }
227
228        public static String createLeftAndRightMatchLikeExpression(String likeExpression) {
229                return "%" + likeExpression.replace("%", "\\%") + "%";
230        }
231
232        public static String createLeftMatchLikeExpression(String likeExpression) {
233                return likeExpression.replace("%", "\\%") + "%";
234        }
235
236        public static String createRightMatchLikeExpression(String likeExpression) {
237                return "%" + likeExpression.replace("%", "\\%");
238        }
239
240
241}