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}