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.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
024import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
025import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
026import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
027import ca.uhn.fhir.jpa.model.dao.JpaPid;
028import ca.uhn.fhir.jpa.search.builder.sql.ColumnTupleObject;
029import ca.uhn.fhir.jpa.search.builder.sql.JpaPidValueTuples;
030import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
031import ca.uhn.fhir.jpa.util.QueryParameterUtils;
032import ca.uhn.fhir.model.api.IQueryParameterType;
033import ca.uhn.fhir.rest.param.TokenParam;
034import ca.uhn.fhir.rest.param.TokenParamModifier;
035import com.healthmarketscience.sqlbuilder.Condition;
036import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
037import jakarta.annotation.Nullable;
038import org.hl7.fhir.instance.model.api.IIdType;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041import org.springframework.beans.factory.annotation.Autowired;
042
043import java.util.HashSet;
044import java.util.LinkedHashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048
049import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
050import static org.apache.commons.lang3.StringUtils.isNotBlank;
051
052public class ResourceIdPredicateBuilder extends BasePredicateBuilder {
053        private static final Logger ourLog = LoggerFactory.getLogger(ResourceIdPredicateBuilder.class);
054
055        @Autowired
056        private IIdHelperService<JpaPid> myIdHelperService;
057
058        /**
059         * Constructor
060         */
061        public ResourceIdPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
062                super(theSearchSqlBuilder);
063        }
064
065        @Nullable
066        public Condition createPredicateResourceId(
067                        @Nullable DbColumn[] theSourceJoinColumn,
068                        String theResourceName,
069                        List<List<IQueryParameterType>> theValues,
070                        SearchFilterParser.CompareOperation theOperation,
071                        RequestPartitionId theRequestPartitionId) {
072
073                Set<JpaPid> allOrPids = null;
074                SearchFilterParser.CompareOperation defaultOperation = SearchFilterParser.CompareOperation.eq;
075
076                for (List<? extends IQueryParameterType> nextValue : theValues) {
077                        Set<IIdType> ids = new LinkedHashSet<>();
078                        boolean haveValue = false;
079                        for (IQueryParameterType next : nextValue) {
080                                String value = next.getValueAsQueryToken(getFhirContext());
081                                if (value != null && value.startsWith("|")) {
082                                        value = value.substring(1);
083                                }
084
085                                if (isNotBlank(value)) {
086                                        haveValue = true;
087                                        if (!value.contains("/")) {
088                                                value = theResourceName + "/" + value;
089                                        }
090                                        IIdType id = getFhirContext().getVersion().newIdType();
091                                        id.setValue(value);
092                                        ids.add(id);
093                                }
094
095                                if (next instanceof TokenParam) {
096                                        if (((TokenParam) next).getModifier() == TokenParamModifier.NOT) {
097                                                defaultOperation = SearchFilterParser.CompareOperation.ne;
098                                        }
099                                }
100                        }
101
102                        Set<JpaPid> orPids = new HashSet<>();
103
104                        // We're joining this to a query that will explicitly ask for non-deleted,
105                        // so we really only want the PID and can safely cache (even if a previously
106                        // deleted status was cached, since it might now be undeleted)
107                        Map<IIdType, IResourceLookup<JpaPid>> resolvedPids = myIdHelperService.resolveResourceIdentities(
108                                        theRequestPartitionId,
109                                        ids,
110                                        ResolveIdentityMode.includeDeleted().cacheOk());
111                        for (IResourceLookup<JpaPid> lookup : resolvedPids.values()) {
112                                orPids.add(lookup.getPersistentId());
113                        }
114
115                        if (haveValue) {
116                                if (allOrPids == null) {
117                                        allOrPids = orPids;
118                                } else {
119                                        allOrPids.retainAll(orPids);
120                                }
121                        }
122                }
123
124                if (allOrPids != null && allOrPids.isEmpty()) {
125
126                        setMatchNothing();
127
128                } else if (allOrPids != null) {
129
130                        SearchFilterParser.CompareOperation operation = defaultIfNull(theOperation, defaultOperation);
131                        assert operation == SearchFilterParser.CompareOperation.eq
132                                        || operation == SearchFilterParser.CompareOperation.ne;
133
134                        if (theSourceJoinColumn == null) {
135                                BaseJoiningPredicateBuilder queryRootTable = super.getOrCreateQueryRootTable(true);
136                                Condition predicate;
137                                switch (operation) {
138                                        default:
139                                        case eq:
140                                                predicate = queryRootTable.createPredicateResourceIds(false, allOrPids);
141                                                break;
142                                        case ne:
143                                                predicate = queryRootTable.createPredicateResourceIds(true, allOrPids);
144                                                break;
145                                }
146                                predicate = queryRootTable.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
147                                return predicate;
148                        } else {
149                                if (getSearchQueryBuilder().isIncludePartitionIdInJoins()) {
150                                        ColumnTupleObject left = new ColumnTupleObject(theSourceJoinColumn);
151                                        JpaPidValueTuples right = JpaPidValueTuples.from(getSearchQueryBuilder(), allOrPids);
152                                        return QueryParameterUtils.toInPredicate(
153                                                        left, right, operation == SearchFilterParser.CompareOperation.ne);
154                                } else {
155                                        DbColumn resIdColumn = getResourceIdColumn(theSourceJoinColumn);
156                                        List<Long> resourceIds = JpaPid.toLongList(allOrPids);
157                                        return QueryParameterUtils.toEqualToOrInPredicate(
158                                                        resIdColumn,
159                                                        generatePlaceholders(resourceIds),
160                                                        operation == SearchFilterParser.CompareOperation.ne);
161                                }
162                        }
163                }
164
165                return null;
166        }
167
168        /**
169         * This method takes 1-2 columns and returns the last one. This is useful where the input is an array of
170         * join columns for SQL Search expressions. In partition key mode, there are 2 columns (partition id and resource id).
171         * In non partition key mode, only the resource id column is used.
172         */
173        @Nullable
174        public static DbColumn getResourceIdColumn(@Nullable DbColumn[] theJoinColumns) {
175                DbColumn resIdColumn;
176                if (theJoinColumns == null) {
177                        return null;
178                } else if (theJoinColumns.length == 1) {
179                        resIdColumn = theJoinColumns[0];
180                } else {
181                        assert theJoinColumns.length == 2;
182                        resIdColumn = theJoinColumns[1];
183                }
184                return resIdColumn;
185        }
186}