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