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}