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.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                RequestPartitionId requestPartitionId =
072                                myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
073                                                theRequestDetails, getResourceName(), theSearchParameterMap);
074                return mySearchCoordinatorSvc.registerSearch(
075                                this,
076                                theSearchParameterMap,
077                                getResourceName(),
078                                new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)),
079                                theRequestDetails,
080                                requestPartitionId);
081        }
082
083        private String getEffectiveParamName() {
084                return Observation.SP_DATE;
085        }
086
087        private String getCodeParamName() {
088                return Observation.SP_CODE;
089        }
090
091        private String getSubjectParamName() {
092                return Observation.SP_SUBJECT;
093        }
094
095        private String getPatientParamName() {
096                return Observation.SP_PATIENT;
097        }
098
099        protected void updateSearchParamsForLastn(
100                        SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails) {
101                if (!isPagingProviderDatabaseBacked(theRequestDetails)) {
102                        theSearchParameterMap.setLoadSynchronous(true);
103                }
104
105                theSearchParameterMap.setLastN(true);
106                SortSpec effectiveDtm = new SortSpec(getEffectiveParamName()).setOrder(SortOrderEnum.DESC);
107                SortSpec observationCode =
108                                new SortSpec(getCodeParamName()).setOrder(SortOrderEnum.ASC).setChain(effectiveDtm);
109                if (theSearchParameterMap.containsKey(getSubjectParamName())
110                                || theSearchParameterMap.containsKey(getPatientParamName())) {
111
112                        new TransactionTemplate(myPlatformTransactionManager)
113                                        .executeWithoutResult(
114                                                        tx -> fixSubjectParamsOrderForLastn(theSearchParameterMap, theRequestDetails));
115
116                        theSearchParameterMap.setSort(new SortSpec(getSubjectParamName())
117                                        .setOrder(SortOrderEnum.ASC)
118                                        .setChain(observationCode));
119                } else {
120                        theSearchParameterMap.setSort(observationCode);
121                }
122        }
123
124        private void fixSubjectParamsOrderForLastn(
125                        SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails) {
126                // Need to ensure that the patient/subject parameters are sorted in the SearchParameterMap to ensure correct
127                // ordering of
128                // the output. The reason for this is that observations are indexed by patient/subject forced ID, but then
129                // ordered in the
130                // final result set by subject/patient resource PID.
131                TreeMap<Long, IQueryParameterType> orderedSubjectReferenceMap = new TreeMap<>();
132                if (theSearchParameterMap.containsKey(getSubjectParamName())) {
133
134                        RequestPartitionId requestPartitionId =
135                                        myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
136                                                        theRequestDetails, getResourceName(), theSearchParameterMap);
137
138                        List<List<IQueryParameterType>> patientParams = new ArrayList<>();
139                        if (theSearchParameterMap.get(getPatientParamName()) != null) {
140                                patientParams.addAll(theSearchParameterMap.get(getPatientParamName()));
141                        }
142                        if (theSearchParameterMap.get(getSubjectParamName()) != null) {
143                                patientParams.addAll(theSearchParameterMap.get(getSubjectParamName()));
144                        }
145
146                        Map<IIdType, ReferenceParam> ids = new HashMap<>();
147                        for (List<? extends IQueryParameterType> nextPatientList : patientParams) {
148                                for (IQueryParameterType nextOr : nextPatientList) {
149                                        if (nextOr instanceof ReferenceParam) {
150                                                ReferenceParam ref = (ReferenceParam) nextOr;
151                                                IIdType id = myFhirContext.getVersion().newIdType();
152                                                id.setParts(null, ref.getResourceType(), ref.getIdPart(), null);
153                                                ids.put(id, ref);
154                                        } else {
155                                                throw new IllegalArgumentException(
156                                                                Msg.code(942) + "Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
157                                        }
158                                }
159                        }
160
161                        Map<IIdType, IResourceLookup<JpaPid>> resolvedIds = myIdHelperService.resolveResourceIdentities(
162                                        requestPartitionId,
163                                        ids.keySet(),
164                                        ResolveIdentityMode.includeDeleted().cacheOk());
165                        for (Map.Entry<IIdType, ReferenceParam> entry : ids.entrySet()) {
166                                IResourceLookup<JpaPid> lookup = resolvedIds.get(entry.getKey());
167                                orderedSubjectReferenceMap.put(lookup.getPersistentId().getId(), entry.getValue());
168                        }
169
170                        theSearchParameterMap.remove(getSubjectParamName());
171                        theSearchParameterMap.remove(getPatientParamName());
172
173                        // Subject PIDs ordered - so create 'OR' list of subjects for lastN operation
174                        ReferenceOrListParam orList = new ReferenceOrListParam();
175                        orderedSubjectReferenceMap
176                                        .keySet()
177                                        .forEach(key -> orList.addOr((ReferenceParam) orderedSubjectReferenceMap.get(key)));
178                        theSearchParameterMap.add(getSubjectParamName(), orList);
179                }
180        }
181}