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}