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;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.dao.ISearchBuilder;
024import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
025import ca.uhn.fhir.jpa.model.dao.JpaPid;
026import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
027import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
028import ca.uhn.fhir.jpa.util.QueryParameterUtils;
029import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
030import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
031import ca.uhn.fhir.rest.api.server.RequestDetails;
032import ca.uhn.fhir.rest.server.method.ResponsePage;
033import jakarta.annotation.Nonnull;
034import org.hl7.fhir.instance.model.api.IBaseResource;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import java.util.List;
039import java.util.Set;
040import java.util.stream.Collectors;
041
042public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider {
043        private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaSearchFirstPageBundleProvider.class);
044        private final SearchTask mySearchTask;
045
046        @SuppressWarnings("rawtypes")
047        private final ISearchBuilder mySearchBuilder;
048
049        /**
050         * Constructor
051         */
052        @SuppressWarnings("rawtypes")
053        public PersistedJpaSearchFirstPageBundleProvider(
054                        SearchTask theSearchTask,
055                        ISearchBuilder theSearchBuilder,
056                        RequestDetails theRequest,
057                        RequestPartitionId theRequestPartitionId) {
058                super(theRequest, theSearchTask.getSearch());
059
060                assert getSearchEntity().getSearchType() != SearchTypeEnum.HISTORY;
061
062                mySearchTask = theSearchTask;
063                mySearchBuilder = theSearchBuilder;
064                super.setRequestPartitionId(theRequestPartitionId);
065        }
066
067        @Nonnull
068        @Override
069        public List<IBaseResource> getResources(
070                        int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder thePageBuilder) {
071                ensureSearchEntityLoaded();
072                QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(getSearchEntity());
073
074                mySearchTask.awaitInitialSync();
075
076                // request 1 more than we need to, in order to know if there are extra values
077                ourLog.trace("Fetching search resource PIDs from task: {}", mySearchTask.getClass());
078                final List<JpaPid> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex + 1);
079                ourLog.trace("Done fetching search resource PIDs");
080
081                int countOfPids = pids.size();
082                ;
083                int maxSize = Math.min(theToIndex - theFromIndex, countOfPids);
084                thePageBuilder.setTotalRequestedResourcesFetched(countOfPids);
085
086                RequestPartitionId requestPartitionId = getRequestPartitionId();
087
088                List<JpaPid> firstBatch = pids.subList(0, maxSize);
089                List<IBaseResource> retVal = myTxService
090                                .withRequest(myRequest)
091                                .withRequestPartitionId(requestPartitionId)
092                                .execute(() -> toResourceList(mySearchBuilder, firstBatch, thePageBuilder));
093
094                long totalCountWanted = theToIndex - theFromIndex;
095                long totalCountMatch = (int) retVal.stream().filter(t -> !isInclude(t)).count();
096
097                if (totalCountMatch < totalCountWanted) {
098                        if (getSearchEntity().getStatus() == SearchStatusEnum.PASSCMPLET
099                                        || ((getSearchEntity().getStatus() == SearchStatusEnum.FINISHED
100                                                        && getSearchEntity().getNumFound() >= theToIndex))) {
101
102                                /*
103                                 * This is a bit of complexity to account for the possibility that
104                                 * the consent service has filtered some results.
105                                 */
106                                Set<String> existingIds = retVal.stream()
107                                                .map(t -> t.getIdElement().getValue())
108                                                .filter(t -> t != null)
109                                                .collect(Collectors.toSet());
110
111                                long remainingWanted = totalCountWanted - totalCountMatch;
112                                long fromIndex = theToIndex - remainingWanted;
113                                ResponsePage.ResponsePageBuilder pageBuilder = new ResponsePage.ResponsePageBuilder();
114                                pageBuilder.setBundleProvider(this);
115                                List<IBaseResource> remaining = super.getResources((int) fromIndex, theToIndex, pageBuilder);
116                                remaining.forEach(t -> {
117                                        if (!existingIds.contains(t.getIdElement().getValue())) {
118                                                retVal.add(t);
119                                        }
120                                });
121                                thePageBuilder.combineWith(pageBuilder);
122                        }
123                }
124                ourLog.trace("Loaded resources to return");
125
126                return retVal;
127        }
128
129        private boolean isInclude(IBaseResource theResource) {
130                BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(theResource);
131                return BundleEntrySearchModeEnum.INCLUDE.equals(searchMode);
132        }
133
134        @Override
135        public Integer size() {
136                ourLog.trace("size() - Waiting for initial sync");
137                Integer size = mySearchTask.awaitInitialSync();
138                ourLog.trace("size() - Finished waiting for local sync");
139
140                ensureSearchEntityLoaded();
141                QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(getSearchEntity());
142                if (size != null) {
143                        return size;
144                }
145                return super.size();
146        }
147}