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