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