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}