001/*-
002 * #%L
003 * HAPI FHIR - Master Data Management
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.mdm.interceptor;
021
022import ca.uhn.fhir.interceptor.api.Hook;
023import ca.uhn.fhir.interceptor.api.Interceptor;
024import ca.uhn.fhir.interceptor.api.Pointcut;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
028import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
029import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc;
030import ca.uhn.fhir.mdm.log.Logs;
031import ca.uhn.fhir.model.api.IQueryParameterType;
032import ca.uhn.fhir.model.primitive.IdDt;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
035import ca.uhn.fhir.rest.param.ReferenceParam;
036import ca.uhn.fhir.rest.param.TokenParam;
037import org.hl7.fhir.instance.model.api.IIdType;
038import org.slf4j.Logger;
039import org.springframework.beans.factory.annotation.Autowired;
040
041import java.util.ArrayList;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045
046/**
047 * This interceptor replaces the auto-generated CapabilityStatement that is generated
048 * by the HAPI FHIR Server with a static hard-coded resource.
049 */
050@Interceptor
051public class MdmSearchExpandingInterceptor {
052        // A simple interface to turn ids into some form of IQueryParameterTypes
053        private interface Creator<T extends IQueryParameterType> {
054                T create(String id);
055        }
056
057        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
058
059        @Autowired
060        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
061
062        @Autowired
063        private IMdmLinkExpandSvc myMdmLinkExpandSvc;
064
065        @Autowired
066        private JpaStorageSettings myStorageSettings;
067
068        @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
069        public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) {
070
071                if (myStorageSettings.isAllowMdmExpansion()) {
072                        final RequestDetails requestDetailsToUse =
073                                        theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails;
074                        final RequestPartitionId requestPartitionId =
075                                        myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
076                                                        requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap);
077                        for (Map.Entry<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) {
078                                String paramName = set.getKey();
079                                List<List<IQueryParameterType>> andList = set.getValue();
080                                for (List<IQueryParameterType> orList : andList) {
081                                        // here we will know if it's an _id param or not
082                                        // from theSearchParameterMap.keySet()
083                                        expandAnyReferenceParameters(requestPartitionId, paramName, orList);
084                                }
085                        }
086                }
087        }
088
089        /**
090         * If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion.
091         */
092        private void expandAnyReferenceParameters(
093                        RequestPartitionId theRequestPartitionId, String theParamName, List<IQueryParameterType> orList) {
094                List<IQueryParameterType> toRemove = new ArrayList<>();
095                List<IQueryParameterType> toAdd = new ArrayList<>();
096                for (IQueryParameterType iQueryParameterType : orList) {
097                        if (iQueryParameterType instanceof ReferenceParam) {
098                                ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
099                                if (refParam.isMdmExpand()) {
100                                        ourLog.debug("Found a reference parameter to expand: {}", refParam);
101                                        // First, attempt to expand as a source resource.
102                                        Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(
103                                                        theRequestPartitionId, new IdDt(refParam.getValue()));
104
105                                        // If we failed, attempt to expand as a golden resource
106                                        if (expandedResourceIds.isEmpty()) {
107                                                expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(
108                                                                theRequestPartitionId, new IdDt(refParam.getValue()));
109                                        }
110
111                                        // Rebuild the search param list.
112                                        if (!expandedResourceIds.isEmpty()) {
113                                                ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
114                                                toRemove.add(refParam);
115                                                expandedResourceIds.stream()
116                                                                .map(resourceId -> addResourceTypeIfNecessary(refParam.getResourceType(), resourceId))
117                                                                .map(ReferenceParam::new)
118                                                                .forEach(toAdd::add);
119                                        }
120                                }
121                        } else if (theParamName.equalsIgnoreCase("_id")) {
122                                expandIdParameter(theRequestPartitionId, iQueryParameterType, toAdd, toRemove);
123                        }
124                }
125
126                orList.removeAll(toRemove);
127                orList.addAll(toAdd);
128        }
129
130        private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) {
131                if (theResourceId.contains("/")) {
132                        return theResourceId;
133                } else {
134                        return theResourceType + "/" + theResourceId;
135                }
136        }
137
138        /**
139         * Expands out the provided _id parameter into all the various
140         * ids of linked resources.
141         *
142         * @param theRequestPartitionId
143         * @param theIdParameter
144         * @param theAddList
145         * @param theRemoveList
146         */
147        private void expandIdParameter(
148                        RequestPartitionId theRequestPartitionId,
149                        IQueryParameterType theIdParameter,
150                        List<IQueryParameterType> theAddList,
151                        List<IQueryParameterType> theRemoveList) {
152                // id parameters can either be StringParam (for $everything operation)
153                // or TokenParam (for searches)
154                // either case, we want to expand it out and grab all related resources
155                IIdType id;
156                Creator<? extends IQueryParameterType> creator;
157                boolean mdmExpand = false;
158                if (theIdParameter instanceof TokenParam) {
159                        TokenParam param = (TokenParam) theIdParameter;
160                        mdmExpand = param.isMdmExpand();
161                        id = new IdDt(param.getValue());
162                        creator = TokenParam::new;
163                } else {
164                        creator = null;
165                        id = null;
166                }
167
168                if (id == null) {
169                        // in case the _id paramter type is different from the above
170                        ourLog.warn(
171                                        "_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!",
172                                        theIdParameter.getClass().getSimpleName());
173                } else if (mdmExpand) {
174                        ourLog.debug("_id parameter must be expanded out from: {}", id.getValue());
175
176                        Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id);
177
178                        if (expandedResourceIds.isEmpty()) {
179                                expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, (IdDt) id);
180                        }
181
182                        // Rebuild
183                        if (!expandedResourceIds.isEmpty()) {
184                                ourLog.debug("_id parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
185
186                                // remove the original
187                                theRemoveList.add(theIdParameter);
188
189                                // add in all the linked values
190                                expandedResourceIds.stream().map(creator::create).forEach(theAddList::add);
191                        }
192                }
193                // else - no expansion required
194        }
195}