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.provider;
021
022import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
025import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
028import ca.uhn.fhir.jpa.interceptor.ProvenanceAgentsPointcutUtil;
029import ca.uhn.fhir.jpa.model.util.JpaConstants;
030import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
031import ca.uhn.fhir.model.api.IProvenanceAgent;
032import ca.uhn.fhir.model.api.annotation.Description;
033import ca.uhn.fhir.model.primitive.IdDt;
034import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest;
035import ca.uhn.fhir.rest.annotation.Operation;
036import ca.uhn.fhir.rest.annotation.OperationParam;
037import ca.uhn.fhir.rest.annotation.Transaction;
038import ca.uhn.fhir.rest.annotation.TransactionParam;
039import ca.uhn.fhir.rest.api.server.RequestDetails;
040import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
041import ca.uhn.fhir.rest.server.provider.ProviderConstants;
042import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
043import ca.uhn.fhir.util.ParametersUtil;
044import jakarta.servlet.http.HttpServletResponse;
045import org.hl7.fhir.instance.model.api.IBaseBundle;
046import org.hl7.fhir.instance.model.api.IBaseParameters;
047import org.hl7.fhir.instance.model.api.IBaseResource;
048import org.hl7.fhir.instance.model.api.IPrimitiveType;
049import org.springframework.beans.factory.annotation.Autowired;
050
051import java.util.Collections;
052import java.util.List;
053import java.util.Map;
054import java.util.TreeMap;
055
056import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK;
057import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID;
058import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID;
059import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
060import static org.apache.commons.lang3.StringUtils.isNotBlank;
061import static software.amazon.awssdk.utils.StringUtils.isBlank;
062
063public final class JpaSystemProvider<T, MT> extends BaseJpaSystemProvider<T, MT> {
064        @Autowired
065        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
066
067        @Autowired
068        private IInterceptorBroadcaster myInterceptorBroadcaster;
069
070        @Description(
071                        "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.")
072        @Operation(
073                        name = MARK_ALL_RESOURCES_FOR_REINDEXING,
074                        idempotent = false,
075                        returnParameters = {@OperationParam(name = "status")})
076        /**
077         * @deprecated
078         * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails)
079         */
080        @Deprecated
081        public IBaseResource markAllResourcesForReindexing(
082                        @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theType) {
083
084                if (theType != null && isNotBlank(theType.getValueAsString())) {
085                        getResourceReindexingSvc().markAllResourcesForReindexing(theType.getValueAsString());
086                } else {
087                        getResourceReindexingSvc().markAllResourcesForReindexing();
088                }
089
090                IBaseParameters retVal = ParametersUtil.newInstance(getContext());
091
092                IPrimitiveType<?> string = ParametersUtil.createString(getContext(), "Marked resources");
093                ParametersUtil.addParameterToParameters(getContext(), retVal, "status", string);
094
095                return retVal;
096        }
097
098        @Description("Forces a single pass of the resource reindexing processor")
099        @Operation(
100                        name = PERFORM_REINDEXING_PASS,
101                        idempotent = false,
102                        returnParameters = {@OperationParam(name = "status")})
103        /**
104         * @deprecated
105         * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails)
106         */
107        @Deprecated
108        public IBaseResource performReindexingPass() {
109                Integer count = getResourceReindexingSvc().runReindexingPass();
110
111                IBaseParameters retVal = ParametersUtil.newInstance(getContext());
112
113                IPrimitiveType<?> string;
114                if (count == null) {
115                        string = ParametersUtil.createString(getContext(), "Index pass already proceeding");
116                } else {
117                        string = ParametersUtil.createString(getContext(), "Indexed " + count + " resources");
118                }
119                ParametersUtil.addParameterToParameters(getContext(), retVal, "status", string);
120
121                return retVal;
122        }
123
124        @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true)
125        @Description(
126                        shortDefinition =
127                                        "Provides the number of resources currently stored on the server, broken down by resource type")
128        public IBaseParameters getResourceCounts() {
129                IBaseParameters retVal = ParametersUtil.newInstance(getContext());
130
131                Map<String, Long> counts = getDao().getResourceCountsFromCache();
132                counts = defaultIfNull(counts, Collections.emptyMap());
133                counts = new TreeMap<>(counts);
134                for (Map.Entry<String, Long> nextEntry : counts.entrySet()) {
135                        ParametersUtil.addParameterToParametersInteger(
136                                        getContext(),
137                                        retVal,
138                                        nextEntry.getKey(),
139                                        nextEntry.getValue().intValue());
140                }
141
142                return retVal;
143        }
144
145        @Operation(
146                        name = ProviderConstants.OPERATION_META,
147                        idempotent = true,
148                        returnParameters = {@OperationParam(name = "return", typeName = "Meta")})
149        public IBaseParameters meta(RequestDetails theRequestDetails) {
150                IBaseParameters retVal = ParametersUtil.newInstance(getContext());
151                ParametersUtil.addParameterToParameters(
152                                getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails));
153                return retVal;
154        }
155
156        @SuppressWarnings("unchecked")
157        @Transaction
158        public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionParam IBaseBundle theResources) {
159                startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());
160                try {
161                        IFhirSystemDao<T, MT> dao = getDao();
162                        return (IBaseBundle) dao.transaction(theRequestDetails, (T) theResources);
163                } finally {
164                        endRequest(((ServletRequestDetails) theRequestDetails).getServletRequest());
165                }
166        }
167
168        @Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true)
169        @Description(
170                        value =
171                                        "This operation searches for all references matching the provided id and updates them to references to the provided target-reference-id.",
172                        shortDefinition = "Repoints referencing resources to another resources instance")
173        public IBaseParameters replaceReferences(
174                        @OperationParam(
175                                                        name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID,
176                                                        min = 1,
177                                                        typeName = "string")
178                                        IPrimitiveType<String> theSourceId,
179                        @OperationParam(
180                                                        name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID,
181                                                        min = 1,
182                                                        typeName = "string")
183                                        IPrimitiveType<String> theTargetId,
184                        @OperationParam(
185                                                        name = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT,
186                                                        typeName = "unsignedInt")
187                                        IPrimitiveType<Integer> theResourceLimit,
188                        ServletRequestDetails theServletRequest) {
189                startRequest(theServletRequest);
190
191                try {
192                        validateReplaceReferencesParams(theSourceId, theTargetId);
193
194                        int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit);
195
196                        IdDt sourceId = new IdDt(theSourceId.getValue());
197                        IdDt targetId = new IdDt(theTargetId.getValue());
198                        RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(
199                                        theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId));
200
201                        List<IProvenanceAgent> provenanceAgents =
202                                        ProvenanceAgentsPointcutUtil.ifHasCallHooks(theServletRequest, myInterceptorBroadcaster);
203
204                        ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest(
205                                        sourceId, targetId, resourceLimit, partitionId, true, provenanceAgents);
206                        IBaseParameters retval =
207                                        getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theServletRequest);
208                        if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK)
209                                        .isPresent()) {
210                                HttpServletResponse response = theServletRequest.getServletResponse();
211                                response.setStatus(HttpServletResponse.SC_ACCEPTED);
212                        }
213                        return retval;
214                } finally {
215                        endRequest(theServletRequest);
216                }
217        }
218
219        private static void validateReplaceReferencesParams(
220                        IPrimitiveType<String> theSourceId, IPrimitiveType<String> theTargetId) {
221                if (theSourceId == null || isBlank(theSourceId.getValue())) {
222                        throw new InvalidRequestException(Msg.code(2583) + "Parameter '"
223                                        + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank");
224                }
225
226                if (theTargetId == null || isBlank(theTargetId.getValue())) {
227                        throw new InvalidRequestException(Msg.code(2584) + "Parameter '"
228                                        + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank");
229                }
230        }
231}