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