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