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}