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