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.dao;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoObservation;
025import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
026import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
027import ca.uhn.fhir.jpa.model.dao.JpaPid;
028import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
029import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
030import ca.uhn.fhir.model.api.IQueryParameterType;
031import ca.uhn.fhir.rest.api.CacheControlDirective;
032import ca.uhn.fhir.rest.api.Constants;
033import ca.uhn.fhir.rest.api.SortOrderEnum;
034import ca.uhn.fhir.rest.api.SortSpec;
035import ca.uhn.fhir.rest.api.server.IBundleProvider;
036import ca.uhn.fhir.rest.api.server.RequestDetails;
037import ca.uhn.fhir.rest.param.ReferenceOrListParam;
038import ca.uhn.fhir.rest.param.ReferenceParam;
039import jakarta.persistence.EntityManager;
040import jakarta.persistence.PersistenceContext;
041import jakarta.persistence.PersistenceContextType;
042import jakarta.servlet.http.HttpServletResponse;
043import org.hl7.fhir.instance.model.api.IBaseResource;
044import org.hl7.fhir.instance.model.api.IIdType;
045import org.hl7.fhir.r4.model.Observation;
046import org.springframework.beans.factory.annotation.Autowired;
047import org.springframework.transaction.support.TransactionTemplate;
048
049import java.util.ArrayList;
050import java.util.HashMap;
051import java.util.List;
052import java.util.Map;
053import java.util.TreeMap;
054
055public class JpaResourceDaoObservation<T extends IBaseResource> extends BaseHapiFhirResourceDao<T>
056                implements IFhirResourceDaoObservation<T> {
057
058        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
059        protected EntityManager myEntityManager;
060
061        @Autowired
062        private IRequestPartitionHelperSvc myRequestPartitionHelperService;
063
064        @Override
065        public IBundleProvider observationsLastN(
066                        SearchParameterMap theSearchParameterMap,
067                        RequestDetails theRequestDetails,
068                        HttpServletResponse theServletResponse) {
069                updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
070
071                return mySearchCoordinatorSvc.registerSearch(
072                                this,
073                                theSearchParameterMap,
074                                getResourceName(),
075                                new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)),
076                                theRequestDetails);
077        }
078
079        private String getEffectiveParamName() {
080                return Observation.SP_DATE;
081        }
082
083        private String getCodeParamName() {
084                return Observation.SP_CODE;
085        }
086
087        private String getSubjectParamName() {
088                return Observation.SP_SUBJECT;
089        }
090
091        private String getPatientParamName() {
092                return Observation.SP_PATIENT;
093        }
094
095        protected void updateSearchParamsForLastn(
096                        SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails) {
097                if (!isPagingProviderDatabaseBacked(theRequestDetails)) {
098                        theSearchParameterMap.setLoadSynchronous(true);
099                }
100
101                theSearchParameterMap.setLastN(true);
102                SortSpec effectiveDtm = new SortSpec(getEffectiveParamName()).setOrder(SortOrderEnum.DESC);
103                SortSpec observationCode =
104                                new SortSpec(getCodeParamName()).setOrder(SortOrderEnum.ASC).setChain(effectiveDtm);
105                if (theSearchParameterMap.containsKey(getSubjectParamName())
106                                || theSearchParameterMap.containsKey(getPatientParamName())) {
107
108                        new TransactionTemplate(myPlatformTransactionManager)
109                                        .executeWithoutResult(
110                                                        tx -> fixSubjectParamsOrderForLastn(theSearchParameterMap, theRequestDetails));
111
112                        theSearchParameterMap.setSort(new SortSpec(getSubjectParamName())
113                                        .setOrder(SortOrderEnum.ASC)
114                                        .setChain(observationCode));
115                } else {
116                        theSearchParameterMap.setSort(observationCode);
117                }
118        }
119
120        private void fixSubjectParamsOrderForLastn(
121                        SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails) {
122                // Need to ensure that the patient/subject parameters are sorted in the SearchParameterMap to ensure correct
123                // ordering of
124                // the output. The reason for this is that observations are indexed by patient/subject forced ID, but then
125                // ordered in the
126                // final result set by subject/patient resource PID.
127                TreeMap<Long, IQueryParameterType> orderedSubjectReferenceMap = new TreeMap<>();
128                if (theSearchParameterMap.containsKey(getSubjectParamName())) {
129
130                        RequestPartitionId requestPartitionId =
131                                        myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
132                                                        theRequestDetails, getResourceName(), theSearchParameterMap);
133
134                        List<List<IQueryParameterType>> patientParams = new ArrayList<>();
135                        if (theSearchParameterMap.get(getPatientParamName()) != null) {
136                                patientParams.addAll(theSearchParameterMap.get(getPatientParamName()));
137                        }
138                        if (theSearchParameterMap.get(getSubjectParamName()) != null) {
139                                patientParams.addAll(theSearchParameterMap.get(getSubjectParamName()));
140                        }
141
142                        Map<IIdType, ReferenceParam> ids = new HashMap<>();
143                        for (List<? extends IQueryParameterType> nextPatientList : patientParams) {
144                                for (IQueryParameterType nextOr : nextPatientList) {
145                                        if (nextOr instanceof ReferenceParam) {
146                                                ReferenceParam ref = (ReferenceParam) nextOr;
147                                                IIdType id = myFhirContext.getVersion().newIdType();
148                                                id.setParts(null, ref.getResourceType(), ref.getIdPart(), null);
149                                                ids.put(id, ref);
150                                        } else {
151                                                throw new IllegalArgumentException(
152                                                                Msg.code(942) + "Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
153                                        }
154                                }
155                        }
156
157                        Map<IIdType, IResourceLookup<JpaPid>> resolvedIds = myIdHelperService.resolveResourceIdentities(
158                                        requestPartitionId,
159                                        ids.keySet(),
160                                        ResolveIdentityMode.includeDeleted().cacheOk());
161                        for (Map.Entry<IIdType, ReferenceParam> entry : ids.entrySet()) {
162                                IResourceLookup<JpaPid> lookup = resolvedIds.get(entry.getKey());
163                                orderedSubjectReferenceMap.put(lookup.getPersistentId().getId(), entry.getValue());
164                        }
165
166                        theSearchParameterMap.remove(getSubjectParamName());
167                        theSearchParameterMap.remove(getPatientParamName());
168
169                        // Subject PIDs ordered - so create 'OR' list of subjects for lastN operation
170                        ReferenceOrListParam orList = new ReferenceOrListParam();
171                        orderedSubjectReferenceMap
172                                        .keySet()
173                                        .forEach(key -> orList.addOr((ReferenceParam) orderedSubjectReferenceMap.get(key)));
174                        theSearchParameterMap.add(getSubjectParamName(), orList);
175                }
176        }
177}