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;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.dao.HistoryBuilder;
024import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory;
025import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser;
026import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
027import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
028import ca.uhn.fhir.model.primitive.InstantDt;
029import ca.uhn.fhir.rest.api.server.IBundleProvider;
030import ca.uhn.fhir.rest.server.method.ResponsePage;
031import jakarta.annotation.Nonnull;
032import jakarta.annotation.Nullable;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IPrimitiveType;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037import org.springframework.beans.factory.annotation.Autowired;
038
039import java.util.ArrayList;
040import java.util.Date;
041import java.util.List;
042import java.util.UUID;
043
044/**
045 * Bundle provider that can search and paginate history entries for specific resource IDs.
046 */
047public class PersistedJpaIdSearchBundleProvider implements IBundleProvider {
048
049        private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaIdSearchBundleProvider.class);
050
051        private final String myUuid;
052        private final @Nonnull String myResourceType;
053        private final @Nonnull List<String> myResourceIds;
054        private final RequestPartitionId myPartitionId;
055        private final Date myRangeStartInclusive;
056        private final @Nonnull Date myRangeEndInclusive;
057
058        @Autowired
059        private HistoryBuilderFactory myHistoryBuilderFactory;
060
061        @Autowired
062        private IJpaStorageResourceParser myJpaStorageResourceParser;
063
064        @Autowired
065        private IHapiTransactionService myTransactionService;
066
067        public PersistedJpaIdSearchBundleProvider(
068                        @Nonnull String theResourceType,
069                        @Nonnull List<String> theResourceIds,
070                        RequestPartitionId thePartitionId,
071                        @Nullable Date theRangeStartInclusive,
072                        @Nonnull Date theRangeEndInclusive) {
073                myUuid = UUID.randomUUID().toString();
074                myResourceType = theResourceType;
075                myResourceIds = theResourceIds;
076                myPartitionId = thePartitionId;
077                myRangeStartInclusive = theRangeStartInclusive;
078                myRangeEndInclusive = theRangeEndInclusive;
079        }
080
081        @Override
082        public IPrimitiveType<Date> getPublished() {
083                return new InstantDt(new Date());
084        }
085
086        @Nonnull
087        @Override
088        public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
089                return getResources(theFromIndex, theToIndex, new ResponsePage.ResponsePageBuilder());
090        }
091
092        @Override
093        public List<IBaseResource> getResources(
094                        int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
095
096                ourLog.debug(
097                                "Invoked with fromIndex={}, toIndex={}, resourceType={}, partitionId={}",
098                                theFromIndex,
099                                theToIndex,
100                                myResourceType,
101                                myPartitionId);
102
103                return myTransactionService.withSystemRequestOnDefaultPartition().execute(() -> {
104                        HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(
105                                        myResourceType, myResourceIds, myRangeStartInclusive, myRangeEndInclusive);
106
107                        RequestPartitionId partitionId = myPartitionId;
108                        if (partitionId == null) {
109                                partitionId = RequestPartitionId.allPartitions();
110                        }
111
112                        // Use null offset and pass indexes directly to get proper pagination
113                        List<ResourceHistoryTable> results =
114                                        historyBuilder.fetchEntities(partitionId, null, theFromIndex, theToIndex, null);
115
116                        ourLog.debug(
117                                        "HistoryBuilder.fetchEntities returned {} results for range {}-{}",
118                                        results.size(),
119                                        theFromIndex,
120                                        theToIndex);
121
122                        List<IBaseResource> retVal = new ArrayList<>();
123                        for (ResourceHistoryTable next : results) {
124                                retVal.add(myJpaStorageResourceParser.toResource(next, true));
125                        }
126
127                        ourLog.debug("returning {} resources", retVal.size());
128
129                        theResponsePageBuilder.setTotalRequestedResourcesFetched(results.size());
130                        return retVal;
131                });
132        }
133
134        @Override
135        public String getUuid() {
136                return myUuid;
137        }
138
139        @Override
140        public Integer preferredPageSize() {
141                return null;
142        }
143
144        @Override
145        public Integer size() {
146                // Return null to allow unlimited pagination
147                return null;
148        }
149}