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}