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