
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}