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