
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.batch2; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 027import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 028import ca.uhn.fhir.jpa.api.pid.IResourcePidList; 029import ca.uhn.fhir.jpa.api.pid.IResourcePidStream; 030import ca.uhn.fhir.jpa.api.pid.StreamTemplate; 031import ca.uhn.fhir.jpa.api.pid.TypedResourcePid; 032import ca.uhn.fhir.jpa.api.pid.TypedResourceStream; 033import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; 034import ca.uhn.fhir.jpa.dao.ISearchBuilder; 035import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; 036import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; 037import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 038import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 039import ca.uhn.fhir.jpa.model.config.PartitionSettings; 040import ca.uhn.fhir.jpa.model.dao.JpaPid; 041import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; 042import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 043import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 044import ca.uhn.fhir.model.primitive.IdDt; 045import ca.uhn.fhir.rest.api.Constants; 046import ca.uhn.fhir.rest.api.SortSpec; 047import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 048import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 049import ca.uhn.fhir.util.DateRangeUtil; 050import ca.uhn.fhir.util.Logs; 051import ca.uhn.fhir.util.UrlUtil; 052import jakarta.annotation.Nonnull; 053import jakarta.annotation.Nullable; 054import org.apache.commons.lang3.StringUtils; 055import org.apache.commons.lang3.Validate; 056import org.hl7.fhir.instance.model.api.IIdType; 057 058import java.util.Date; 059import java.util.UUID; 060import java.util.function.Supplier; 061import java.util.stream.Stream; 062 063import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 064 065public class Batch2DaoSvcImpl implements IBatch2DaoSvc { 066 private static final org.slf4j.Logger ourLog = Logs.getBatchTroubleshootingLog(); 067 068 private final IResourceTableDao myResourceTableDao; 069 070 private final IResourceLinkDao myResourceLinkDao; 071 072 private final MatchUrlService myMatchUrlService; 073 074 private final DaoRegistry myDaoRegistry; 075 076 private final FhirContext myFhirContext; 077 078 private final IHapiTransactionService myTransactionService; 079 080 private final PartitionSettings myPartitionSettings; 081 082 private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory; 083 084 @Override 085 public boolean isAllResourceTypeSupported() { 086 return true; 087 } 088 089 public Batch2DaoSvcImpl( 090 IResourceTableDao theResourceTableDao, 091 IResourceLinkDao theResourceLinkDao, 092 MatchUrlService theMatchUrlService, 093 DaoRegistry theDaoRegistry, 094 FhirContext theFhirContext, 095 IHapiTransactionService theTransactionService, 096 PartitionSettings thePartitionSettings, 097 SearchBuilderFactory<JpaPid> theSearchBuilderFactory) { 098 myResourceTableDao = theResourceTableDao; 099 myResourceLinkDao = theResourceLinkDao; 100 myMatchUrlService = theMatchUrlService; 101 myDaoRegistry = theDaoRegistry; 102 myFhirContext = theFhirContext; 103 myTransactionService = theTransactionService; 104 myPartitionSettings = thePartitionSettings; 105 mySearchBuilderFactory = theSearchBuilderFactory; 106 } 107 108 @Override 109 public IResourcePidStream fetchResourceIdStream( 110 Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId, String theUrl) { 111 112 if (StringUtils.isBlank(theUrl)) { 113 // first scenario 114 return makeStreamResult( 115 theRequestPartitionId, () -> streamResourceIdsNoUrl(theStart, theEnd, theRequestPartitionId)); 116 } else { 117 return makeStreamResult( 118 theRequestPartitionId, 119 () -> streamResourceIdsWithUrl(theStart, theEnd, theUrl, theRequestPartitionId)); 120 } 121 } 122 123 @Override 124 public Stream<IdDt> streamSourceIdsThatReferenceTargetId(IIdType theTargetId) { 125 return myResourceLinkDao.streamSourceIdsForTargetFhirId(theTargetId.getResourceType(), theTargetId.getIdPart()); 126 } 127 128 private Stream<TypedResourcePid> streamResourceIdsWithUrl( 129 Date theStart, Date theEnd, String theUrl, RequestPartitionId theRequestPartitionId) { 130 validateUrl(theUrl); 131 132 String resourceType = UrlUtil.determineResourceTypeInResourceUrl(myFhirContext, theUrl); 133 134 // Search in all partitions if no partition is provided 135 ourLog.debug("No partition id detected in request - searching all partitions"); 136 RequestPartitionId thePartitionId = defaultIfNull(theRequestPartitionId, RequestPartitionId.allPartitions()); 137 138 SearchParameterMap searchParamMap; 139 SystemRequestDetails request = new SystemRequestDetails(); 140 request.setRequestPartitionId(thePartitionId); 141 142 if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) { 143 searchParamMap = parseQuery(theUrl, null); 144 } else { 145 searchParamMap = parseQuery(theUrl); 146 } 147 148 searchParamMap.setLastUpdated(DateRangeUtil.narrowDateRange(searchParamMap.getLastUpdated(), theStart, theEnd)); 149 150 if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) { 151 return searchForResourceIdsAndType(thePartitionId, request, searchParamMap); 152 } 153 154 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType); 155 156 return dao.searchForIdStream(searchParamMap, request, null).map(pid -> new TypedResourcePid(resourceType, pid)); 157 } 158 159 /** 160 * Since the resource type is not specified, query the DB for resources matching the params and return resource ID and type 161 * 162 * @param theRequestPartitionId the partition to search on 163 * @param theRequestDetails the theRequestDetails details 164 * @param theSearchParams the search params 165 * @return Stream of typed resource pids 166 */ 167 private Stream<TypedResourcePid> searchForResourceIdsAndType( 168 RequestPartitionId theRequestPartitionId, 169 SystemRequestDetails theRequestDetails, 170 SearchParameterMap theSearchParams) { 171 ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(null, null); 172 return myTransactionService 173 .withRequest(theRequestDetails) 174 .search(() -> builder.createQueryStream( 175 theSearchParams, 176 new SearchRuntimeDetails( 177 theRequestDetails, UUID.randomUUID().toString()), 178 theRequestDetails, 179 theRequestPartitionId)) 180 .map(pid -> new TypedResourcePid(pid.getResourceType(), pid)); 181 } 182 183 @Nonnull 184 private TypedResourceStream makeStreamResult( 185 RequestPartitionId theRequestPartitionId, Supplier<Stream<TypedResourcePid>> streamSupplier) { 186 187 IHapiTransactionService.IExecutionBuilder txSettings = 188 myTransactionService.withSystemRequest().withRequestPartitionId(theRequestPartitionId); 189 190 StreamTemplate<TypedResourcePid> streamTemplate = 191 StreamTemplate.fromSupplier(streamSupplier).withTransactionAdvice(txSettings); 192 193 return new TypedResourceStream(theRequestPartitionId, streamTemplate); 194 } 195 196 /** 197 * At the moment there is no use-case for this method. 198 * This can be cleaned up at a later point in time if there is no use for it. 199 */ 200 @Nonnull 201 private Stream<TypedResourcePid> streamResourceIdsNoUrl( 202 Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId) { 203 Integer defaultPartitionId = myPartitionSettings.getDefaultPartitionId(); 204 Stream<Object[]> rowStream; 205 206 if (theRequestPartitionId == null || theRequestPartitionId.isAllPartitions()) { 207 ourLog.debug("Search for resources - all partitions"); 208 rowStream = myResourceTableDao.streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldest( 209 theStart, theEnd); 210 } else if (theRequestPartitionId.isPartition(defaultPartitionId)) { 211 ourLog.debug("Search for resources - default partition"); 212 rowStream = 213 myResourceTableDao 214 .streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldestForDefaultPartition( 215 theStart, theEnd, defaultPartitionId); 216 } else { 217 ourLog.debug("Search for resources - partition {}", theRequestPartitionId); 218 rowStream = 219 myResourceTableDao 220 .streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldestForPartitionIds( 221 theStart, theEnd, theRequestPartitionId.getPartitionIds()); 222 } 223 224 return rowStream.map(Batch2DaoSvcImpl::typedPidFromQueryArray); 225 } 226 227 @Deprecated(since = "6.11", forRemoval = true) // delete once the default method in the interface is gone. 228 @Override 229 public IResourcePidList fetchResourceIdsPage( 230 Date theStart, Date theEnd, @Nullable RequestPartitionId theRequestPartitionId, @Nullable String theUrl) { 231 Validate.isTrue(false, "Unimplemented"); 232 return null; 233 } 234 235 @Nonnull 236 private SearchParameterMap parseQuery(String theUrl) { 237 String resourceType = theUrl.substring(0, theUrl.indexOf('?')); 238 RuntimeResourceDefinition def = myFhirContext.getResourceDefinition(resourceType); 239 240 return parseQuery(theUrl, def); 241 } 242 243 @Nonnull 244 private SearchParameterMap parseQuery( 245 String theUrl, @Nullable RuntimeResourceDefinition theRuntimeResourceDefinition) { 246 SearchParameterMap searchParamMap = myMatchUrlService.translateMatchUrl(theUrl, theRuntimeResourceDefinition); 247 // this matches idx_res_type_del_updated 248 searchParamMap.setSort(new SortSpec(Constants.PARAM_LASTUPDATED).setChain(new SortSpec(Constants.PARAM_PID))); 249 // TODO this limits us to 2G resources. 250 searchParamMap.setLoadSynchronousUpTo(Integer.MAX_VALUE); 251 return searchParamMap; 252 } 253 254 private static TypedResourcePid typedPidFromQueryArray(Object[] thePidTypeDateArray) { 255 JpaPid pid = (JpaPid) thePidTypeDateArray[0]; 256 String resourceType = (String) thePidTypeDateArray[1]; 257 return new TypedResourcePid(resourceType, pid); 258 } 259 260 private static void validateUrl(@Nonnull String theUrl) { 261 if (!theUrl.contains("?")) { 262 throw new InternalErrorException(Msg.code(2422) + "this should never happen: URL is missing a '?'"); 263 } 264 } 265 266 private static boolean isNoResourceTypeProvidedInUrl(String theUrl, String resourceType) { 267 return (resourceType == null || resourceType.isBlank()) && theUrl.indexOf('?') == 0; 268 } 269}