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}