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