
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}