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