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.i18n.Msg; 023import ca.uhn.fhir.interceptor.api.HookParams; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; 027import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; 028import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 029import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; 030import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 031import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 032import ca.uhn.fhir.jpa.util.QueryParameterUtils; 033import ca.uhn.fhir.model.api.IQueryParameterType; 034import ca.uhn.fhir.rest.api.server.RequestDetails; 035import ca.uhn.fhir.rest.param.UriParam; 036import ca.uhn.fhir.rest.param.UriParamQualifierEnum; 037import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 038import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 039import com.healthmarketscience.sqlbuilder.BinaryCondition; 040import com.healthmarketscience.sqlbuilder.ComboCondition; 041import com.healthmarketscience.sqlbuilder.Condition; 042import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045import org.springframework.beans.factory.annotation.Autowired; 046 047import java.util.ArrayList; 048import java.util.Collection; 049import java.util.List; 050 051import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftAndRightMatchLikeExpression; 052import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftMatchLikeExpression; 053import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createRightMatchLikeExpression; 054 055public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder { 056 057 private static final Logger ourLog = LoggerFactory.getLogger(UriPredicateBuilder.class); 058 private final DbColumn myColumnUri; 059 private final DbColumn myColumnHashUri; 060 061 @Autowired 062 private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; 063 064 @Autowired 065 private IInterceptorBroadcaster myInterceptorBroadcaster; 066 067 /** 068 * Constructor 069 */ 070 public UriPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) { 071 super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_URI")); 072 073 myColumnUri = getTable().addColumn("SP_URI"); 074 myColumnHashUri = getTable().addColumn("HASH_URI"); 075 } 076 077 public Condition addPredicate( 078 List<? extends IQueryParameterType> theUriOrParameterList, 079 String theParamName, 080 SearchFilterParser.CompareOperation theOperation, 081 RequestDetails theRequestDetails) { 082 083 List<Condition> codePredicates = new ArrayList<>(); 084 boolean predicateIsHash = false; 085 for (IQueryParameterType nextOr : theUriOrParameterList) { 086 087 if (nextOr instanceof UriParam) { 088 UriParam param = (UriParam) nextOr; 089 090 String value = param.getValue(); 091 if (value == null) { 092 continue; 093 } 094 095 if (param.getQualifier() == UriParamQualifierEnum.ABOVE) { 096 097 /* 098 * :above is an inefficient query- It means that the user is supplying a more specific URL (say 099 * http://example.com/foo/bar/baz) and that we should match on any URLs that are less 100 * specific but otherwise the same. For example http://example.com and http://example.com/foo would both 101 * match. 102 * 103 * We do this by querying the DB for all candidate URIs and then manually checking each one. This isn't 104 * very efficient, but this is also probably not a very common type of query to do. 105 * 106 * If we ever need to make this more efficient, lucene could certainly be used as an optimization. 107 */ 108 String msg = "Searching for candidate URI:above parameters for Resource[" + getResourceType() 109 + "] param[" + theParamName + "]"; 110 ourLog.info(msg); 111 112 IInterceptorBroadcaster compositeBroadcaster = 113 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 114 myInterceptorBroadcaster, theRequestDetails); 115 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { 116 StorageProcessingMessage message = new StorageProcessingMessage(); 117 message.setMessage(msg); 118 HookParams params = new HookParams() 119 .add(RequestDetails.class, theRequestDetails) 120 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 121 .add(StorageProcessingMessage.class, message); 122 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 123 } 124 125 long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity( 126 getPartitionSettings(), getRequestPartitionId(), getResourceType(), theParamName); 127 Collection<String> candidates = 128 myResourceIndexedSearchParamUriDao.findAllByHashIdentity(hashIdentity); 129 List<String> toFind = new ArrayList<>(); 130 for (String next : candidates) { 131 if (value.length() >= next.length()) { 132 if (value.startsWith(next)) { 133 toFind.add(next); 134 } 135 } 136 } 137 138 if (toFind.isEmpty()) { 139 continue; 140 } 141 142 Condition uriPredicate = 143 QueryParameterUtils.toEqualToOrInPredicate(myColumnUri, generatePlaceholders(toFind)); 144 Condition hashAndUriPredicate = 145 combineWithHashIdentityPredicate(getResourceType(), theParamName, uriPredicate); 146 codePredicates.add(hashAndUriPredicate); 147 148 } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { 149 150 Condition uriPredicate = BinaryCondition.like( 151 myColumnUri, generatePlaceholder(createLeftMatchLikeExpression(value))); 152 Condition hashAndUriPredicate = 153 combineWithHashIdentityPredicate(getResourceType(), theParamName, uriPredicate); 154 codePredicates.add(hashAndUriPredicate); 155 156 } else { 157 158 Condition uriPredicate = null; 159 if (theOperation == null || theOperation == SearchFilterParser.CompareOperation.eq) { 160 long hashUri = ResourceIndexedSearchParamUri.calculateHashUri( 161 getPartitionSettings(), 162 getRequestPartitionId(), 163 getResourceType(), 164 theParamName, 165 value); 166 uriPredicate = BinaryCondition.equalTo(myColumnHashUri, generatePlaceholder(hashUri)); 167 predicateIsHash = true; 168 } else if (theOperation == SearchFilterParser.CompareOperation.ne) { 169 uriPredicate = BinaryCondition.notEqualTo(myColumnUri, generatePlaceholder(value)); 170 } else if (theOperation == SearchFilterParser.CompareOperation.co) { 171 uriPredicate = BinaryCondition.like( 172 myColumnUri, generatePlaceholder(createLeftAndRightMatchLikeExpression(value))); 173 } else if (theOperation == SearchFilterParser.CompareOperation.gt) { 174 uriPredicate = BinaryCondition.greaterThan(myColumnUri, generatePlaceholder(value)); 175 } else if (theOperation == SearchFilterParser.CompareOperation.lt) { 176 uriPredicate = BinaryCondition.lessThan(myColumnUri, generatePlaceholder(value)); 177 } else if (theOperation == SearchFilterParser.CompareOperation.ge) { 178 uriPredicate = BinaryCondition.greaterThanOrEq(myColumnUri, generatePlaceholder(value)); 179 } else if (theOperation == SearchFilterParser.CompareOperation.le) { 180 uriPredicate = BinaryCondition.lessThanOrEq(myColumnUri, generatePlaceholder(value)); 181 } else if (theOperation == SearchFilterParser.CompareOperation.sw) { 182 uriPredicate = BinaryCondition.like( 183 myColumnUri, generatePlaceholder(createLeftMatchLikeExpression(value))); 184 } else if (theOperation == SearchFilterParser.CompareOperation.ew) { 185 uriPredicate = BinaryCondition.like( 186 myColumnUri, generatePlaceholder(createRightMatchLikeExpression(value))); 187 } else { 188 throw new IllegalArgumentException(Msg.code(1226) 189 + String.format("Unsupported operator specified in _filter clause, %s", theOperation)); 190 } 191 192 codePredicates.add(uriPredicate); 193 } 194 195 } else { 196 throw new IllegalArgumentException(Msg.code(1227) + "Invalid URI type: expected " 197 + UriParam.class.getName() + ", but was " + nextOr.getClass()); 198 } 199 } 200 201 /* 202 * If we haven't found any of the requested URIs in the candidates, then the whole query should match nothing 203 */ 204 if (codePredicates.isEmpty()) { 205 setMatchNothing(); 206 return null; 207 } 208 209 ComboCondition orPredicate = ComboCondition.or(codePredicates.toArray(new Condition[0])); 210 if (predicateIsHash) { 211 return orPredicate; 212 } else { 213 return combineWithHashIdentityPredicate(getResourceType(), theParamName, orPredicate); 214 } 215 } 216 217 public DbColumn getColumnValue() { 218 return myColumnUri; 219 } 220}