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