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 StorageProcessingMessage message = new StorageProcessingMessage(); 113 ourLog.warn(msg); 114 message.setMessage(msg); 115 HookParams params = new HookParams() 116 .add(RequestDetails.class, theRequestDetails) 117 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 118 .add(StorageProcessingMessage.class, message); 119 CompositeInterceptorBroadcaster.doCallHooks( 120 myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_WARNING, params); 121 122 long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity( 123 getPartitionSettings(), getRequestPartitionId(), getResourceType(), theParamName); 124 Collection<String> candidates = 125 myResourceIndexedSearchParamUriDao.findAllByHashIdentity(hashIdentity); 126 List<String> toFind = new ArrayList<>(); 127 for (String next : candidates) { 128 if (value.length() >= next.length()) { 129 if (value.startsWith(next)) { 130 toFind.add(next); 131 } 132 } 133 } 134 135 if (toFind.isEmpty()) { 136 continue; 137 } 138 139 Condition uriPredicate = 140 QueryParameterUtils.toEqualToOrInPredicate(myColumnUri, generatePlaceholders(toFind)); 141 Condition hashAndUriPredicate = 142 combineWithHashIdentityPredicate(getResourceType(), theParamName, uriPredicate); 143 codePredicates.add(hashAndUriPredicate); 144 145 } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { 146 147 Condition uriPredicate = BinaryCondition.like( 148 myColumnUri, generatePlaceholder(createLeftMatchLikeExpression(value))); 149 Condition hashAndUriPredicate = 150 combineWithHashIdentityPredicate(getResourceType(), theParamName, uriPredicate); 151 codePredicates.add(hashAndUriPredicate); 152 153 } else { 154 155 Condition uriPredicate = null; 156 if (theOperation == null || theOperation == SearchFilterParser.CompareOperation.eq) { 157 long hashUri = ResourceIndexedSearchParamUri.calculateHashUri( 158 getPartitionSettings(), 159 getRequestPartitionId(), 160 getResourceType(), 161 theParamName, 162 value); 163 uriPredicate = BinaryCondition.equalTo(myColumnHashUri, generatePlaceholder(hashUri)); 164 predicateIsHash = true; 165 } else if (theOperation == SearchFilterParser.CompareOperation.ne) { 166 uriPredicate = BinaryCondition.notEqualTo(myColumnUri, generatePlaceholder(value)); 167 } else if (theOperation == SearchFilterParser.CompareOperation.co) { 168 uriPredicate = BinaryCondition.like( 169 myColumnUri, generatePlaceholder(createLeftAndRightMatchLikeExpression(value))); 170 } else if (theOperation == SearchFilterParser.CompareOperation.gt) { 171 uriPredicate = BinaryCondition.greaterThan(myColumnUri, generatePlaceholder(value)); 172 } else if (theOperation == SearchFilterParser.CompareOperation.lt) { 173 uriPredicate = BinaryCondition.lessThan(myColumnUri, generatePlaceholder(value)); 174 } else if (theOperation == SearchFilterParser.CompareOperation.ge) { 175 uriPredicate = BinaryCondition.greaterThanOrEq(myColumnUri, generatePlaceholder(value)); 176 } else if (theOperation == SearchFilterParser.CompareOperation.le) { 177 uriPredicate = BinaryCondition.lessThanOrEq(myColumnUri, generatePlaceholder(value)); 178 } else if (theOperation == SearchFilterParser.CompareOperation.sw) { 179 uriPredicate = BinaryCondition.like( 180 myColumnUri, generatePlaceholder(createLeftMatchLikeExpression(value))); 181 } else if (theOperation == SearchFilterParser.CompareOperation.ew) { 182 uriPredicate = BinaryCondition.like( 183 myColumnUri, generatePlaceholder(createRightMatchLikeExpression(value))); 184 } else { 185 throw new IllegalArgumentException(Msg.code(1226) 186 + String.format("Unsupported operator specified in _filter clause, %s", theOperation)); 187 } 188 189 codePredicates.add(uriPredicate); 190 } 191 192 } else { 193 throw new IllegalArgumentException(Msg.code(1227) + "Invalid URI type: expected " 194 + UriParam.class.getName() + ", but was " + nextOr.getClass()); 195 } 196 } 197 198 /* 199 * If we haven't found any of the requested URIs in the candidates, then the whole query should match nothing 200 */ 201 if (codePredicates.isEmpty()) { 202 setMatchNothing(); 203 return null; 204 } 205 206 ComboCondition orPredicate = ComboCondition.or(codePredicates.toArray(new Condition[0])); 207 if (predicateIsHash) { 208 return orPredicate; 209 } else { 210 return combineWithHashIdentityPredicate(getResourceType(), theParamName, orPredicate); 211 } 212 } 213 214 public DbColumn getColumnValue() { 215 return myColumnUri; 216 } 217}