001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.support.IValidationSupport;
025import ca.uhn.fhir.model.api.StorageResponseCodeEnum;
026import ca.uhn.fhir.util.FhirTerser;
027import ca.uhn.fhir.util.HapiExtensions;
028import ca.uhn.fhir.util.MetaUtil;
029import jakarta.annotation.Nullable;
030import org.hl7.fhir.instance.model.api.IBase;
031import org.hl7.fhir.instance.model.api.IBaseBundle;
032import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
033import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
034import org.hl7.fhir.instance.model.api.IBaseResource;
035import org.hl7.fhir.instance.model.api.IIdType;
036
037import java.util.ArrayList;
038import java.util.Collections;
039import java.util.List;
040
041import static org.apache.commons.lang3.StringUtils.isBlank;
042import static org.apache.commons.lang3.StringUtils.isNotBlank;
043
044/**
045 * This class contains utility methods for working with HAPI FHIR Transactions (referring to the FHIR
046 * "transaction" operation, as opposed to working with database transactions).
047 */
048public class TransactionUtil {
049
050        /**
051         * Non instantiable
052         */
053        private TransactionUtil() {
054                super();
055        }
056
057        /**
058         * This method accepts a {@literal Bundle} which was returned by a call to HAPI FHIR's
059         * transaction/batch processor. HAPI FHIR has specific codes and extensions it will
060         * always put into the OperationOutcomes returned by the transaction processor, so
061         * this method parses these and returns a more machine-processable interpretation.
062         * <p>
063         * This method only returns outcome details for standard write operations (create/update/patch)
064         * and will not include details for other kinds of operations (read/search/etc).
065         * </p>
066         * <p>
067         * This method should only be called for Bundles returned by HAPI FHIR's transaction
068         * processor, results will have no meaning for any other input.
069         * </p>
070         *
071         * @param theContext                   A FhirContext instance for the appropriate FHIR version
072         * @param theTransactionRequestBundle  The transaction request bundle which was processed in order to produce {@literal theTransactionResponseBundle}
073         * @param theTransactionResponseBundle The transaction response bundle. This bundle must be a transaction response produced by the same version of
074         *                                     HAPI FHIR, as this code looks for elements it will expect to be present.
075         * @since 8.2.0
076         */
077        public static TransactionResponse parseTransactionResponse(
078                        FhirContext theContext, IBaseBundle theTransactionRequestBundle, IBaseBundle theTransactionResponseBundle) {
079                FhirTerser terser = theContext.newTerser();
080                List<StorageOutcome> storageOutcomes = new ArrayList<>();
081
082                String bundleMetaSource = null;
083                if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
084                        bundleMetaSource = MetaUtil.getSource(theContext, theTransactionRequestBundle);
085                }
086
087                List<IBase> requestEntries = terser.getValues(theTransactionRequestBundle, "entry");
088                List<IBase> responseEntries = terser.getValues(theTransactionResponseBundle, "entry");
089                for (int i = 0; i < responseEntries.size(); i++) {
090
091                        /*
092                         * Transaction response bundles will always have one entry per request
093                         * bundle entry, and those entries will always be in the same order. This
094                         * is a FHIR rule, and HAPI makes sure to always honour it.
095                         */
096                        IBase requestEntry = requestEntries.get(i);
097                        IBase responseEntry = responseEntries.get(i);
098
099                        String requestVerb = terser.getSinglePrimitiveValueOrNull(requestEntry, "request.method");
100                        if ("GET".equals(requestVerb)) {
101                                continue;
102                        }
103
104                        IBaseResource requestResource = terser.getSingleValueOrNull(requestEntry, "resource", IBaseResource.class);
105                        String requestMetaSource = null;
106                        if (requestResource != null
107                                        && theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
108                                requestMetaSource = MetaUtil.getSource(theContext, requestResource);
109                        }
110                        if (isBlank(requestMetaSource)) {
111                                requestMetaSource = bundleMetaSource;
112                        }
113
114                        String requestFullUrlString = terser.getSinglePrimitiveValueOrNull(requestEntry, "fullUrl");
115                        IIdType requestFullUrl = null;
116                        if (requestFullUrlString != null) {
117                                requestFullUrl = theContext.getVersion().newIdType(requestFullUrlString);
118                                requestFullUrl = toUnqualified(requestFullUrl);
119                        }
120
121                        IBase responseResponse = terser.getSingleValueOrNull(responseEntry, "response", IBase.class);
122                        if (responseResponse != null) {
123
124                                // As long as we're parsing a bundle from HAPI, this will never be used. But let's be
125                                // defensive just in case.
126                                int statusCode = 0;
127                                String statusMessage = terser.getSinglePrimitiveValueOrNull(responseResponse, "status");
128                                if (statusMessage != null) {
129                                        int statusSpaceIdx = statusMessage.indexOf(' ');
130                                        if (statusSpaceIdx > 0) {
131                                                statusCode = Integer.parseInt(statusMessage.substring(0, statusSpaceIdx));
132                                        }
133                                }
134
135                                List<IBase> issues = Collections.emptyList();
136                                if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
137                                        issues = terser.getValues(responseResponse, "outcome.issue");
138                                } else {
139                                        IBaseResource responseResource =
140                                                        terser.getSingleValueOrNull(responseEntry, "resource", IBaseResource.class);
141                                        if (responseResource instanceof IBaseOperationOutcome) {
142                                                issues = terser.getValues(responseResource, "issue");
143                                        }
144                                }
145                                IIdType groupSourceId = null;
146                                for (int issueIndex = 0; issueIndex < issues.size(); issueIndex++) {
147                                        IBase issue = issues.get(issueIndex);
148                                        IIdType sourceId = requestFullUrl;
149
150                                        String outcomeSystem = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.system");
151                                        StorageResponseCodeEnum responseCode = null;
152                                        if (StorageResponseCodeEnum.SYSTEM.equals(outcomeSystem)) {
153                                                String outcomeCode = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.code");
154                                                responseCode = StorageResponseCodeEnum.valueOf(outcomeCode);
155                                        }
156
157                                        String errorMessage = null;
158                                        String issueSeverityString = terser.getSinglePrimitiveValueOrNull(issue, "severity");
159                                        if (isNotBlank(issueSeverityString)) {
160                                                IValidationSupport.IssueSeverity issueSeverity =
161                                                                IValidationSupport.IssueSeverity.fromCode(issueSeverityString);
162                                                if (issueSeverity != null) {
163                                                        if (issueSeverity.ordinal() <= IValidationSupport.IssueSeverity.ERROR.ordinal()) {
164                                                                errorMessage = terser.getSinglePrimitiveValueOrNull(issue, "diagnostics");
165                                                        }
166                                                }
167                                        }
168
169                                        if (responseCode == null && statusCode >= 400 && statusCode <= 599) {
170                                                responseCode = StorageResponseCodeEnum.FAILURE;
171                                        }
172
173                                        IIdType targetId;
174                                        if (responseCode == StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE) {
175                                                /*
176                                                 * The first issue on a transaction response OO will have the details about the
177                                                 * processing of the actual input resource that was in the input transaction bundle.
178                                                 * However, if any automatically created placeholders were created during the
179                                                 * processing of that resource, details about those will be placed in subsequent
180                                                 * issues.
181                                                 */
182
183                                                targetId = ((IBaseHasExtensions) issue)
184                                                                .getExtension().stream()
185                                                                                .filter(t -> HapiExtensions.EXTENSION_PLACEHOLDER_ID.equals(t.getUrl()))
186                                                                                .findFirst()
187                                                                                .map(t -> (IIdType) t.getValue())
188                                                                                .orElse(null);
189                                                sourceId = groupSourceId;
190                                        } else {
191                                                String responseLocation =
192                                                                terser.getSinglePrimitiveValueOrNull(responseEntry, "response.location");
193                                                if (isNotBlank(responseLocation)) {
194                                                        targetId = theContext.getVersion().newIdType(responseLocation);
195                                                        if (issueIndex == 0) {
196                                                                groupSourceId = targetId;
197                                                        }
198                                                } else {
199                                                        targetId = null;
200                                                }
201                                        }
202
203                                        StorageOutcome outcome = new StorageOutcome(
204                                                        statusCode,
205                                                        statusMessage,
206                                                        responseCode,
207                                                        toUnqualified(sourceId),
208                                                        toUnqualified(targetId),
209                                                        errorMessage,
210                                                        requestMetaSource);
211                                        storageOutcomes.add(outcome);
212                                }
213                        }
214                }
215
216                return new TransactionResponse(storageOutcomes);
217        }
218
219        private static IIdType toUnqualified(@Nullable IIdType theId) {
220                if (theId != null && theId.hasBaseUrl()) {
221                        return theId.toUnqualified();
222                }
223                return theId;
224        }
225
226        /**
227         * @see #parseTransactionResponse(FhirContext, IBaseBundle, IBaseBundle)
228         */
229        public static class TransactionResponse {
230
231                private final List<StorageOutcome> myStorageOutcomes;
232
233                public TransactionResponse(List<StorageOutcome> theStorageOutcomes) {
234                        myStorageOutcomes = theStorageOutcomes;
235                }
236
237                public List<StorageOutcome> getStorageOutcomes() {
238                        return myStorageOutcomes;
239                }
240        }
241
242        /**
243         * @see #parseTransactionResponse(FhirContext, IBaseBundle, IBaseBundle)
244         */
245        public static class StorageOutcome {
246                private final StorageResponseCodeEnum myStorageResponseCode;
247                private final IIdType myTargetId;
248                private final IIdType mySourceId;
249                private final int myStatusCode;
250                private final String myErrorMessage;
251                private final String myRequestMetaSource;
252                private final String myStatusMessage;
253
254                public StorageOutcome(
255                                int theStatusCode,
256                                String theStatusMessage,
257                                StorageResponseCodeEnum theStorageResponseCode,
258                                IIdType theSourceId,
259                                IIdType theTargetId,
260                                String theErrorMessage,
261                                String theRequestMetaSource) {
262                        myStatusCode = theStatusCode;
263                        myStatusMessage = theStatusMessage;
264                        myStorageResponseCode = theStorageResponseCode;
265                        myTargetId = theTargetId;
266                        mySourceId = theSourceId;
267                        myErrorMessage = theErrorMessage;
268                        myRequestMetaSource = theRequestMetaSource;
269                }
270
271                /**
272                 * @return Returns an error message if the specific action resulted in a failure. Returns {@literal null}
273                 *      otherwise.
274                 */
275                public String getErrorMessage() {
276                        return myErrorMessage;
277                }
278
279                /**
280                 * @return Returns the HTTP status code
281                 */
282                public int getStatusCode() {
283                        return myStatusCode;
284                }
285
286                /**
287                 * @return Returns the complete HTTP status message including the {@link #getStatusCode()} and the rest of the message. For example: {@literal 200 OK}
288                 */
289                public String getStatusMessage() {
290                        return myStatusMessage;
291                }
292
293                /**
294                 * @return Contains a code identifying the specific outcome of this operation.
295                 */
296                public StorageResponseCodeEnum getStorageResponseCode() {
297                        return myStorageResponseCode;
298                }
299
300                /**
301                 * @return Returns the ID of the resource as it was stored in the repository.
302                 */
303                public IIdType getTargetId() {
304                        return myTargetId;
305                }
306
307                /**
308                 * @return Returns the ID of the resource in the request bundle in most cases. This could be an actual
309                 *      resource ID if the operation was an update by ID, or a placeholder UUID if placeholder IDs were in
310                 *      use in the bundle. If the {@link #getStorageResponseCode()} for this outcome is
311                 *    {@link StorageResponseCodeEnum#AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE}, the source ID will be the
312                 *      actual resolved and stored resource ID of the resource containing the reference which caused the
313                 *      placeholder to be created. The ID returned will be unqualified, meaning it has no base URL.
314                 */
315                public IIdType getSourceId() {
316                        return mySourceId;
317                }
318
319                /**
320                 * @return Returns the <code>Resource.meta.source</code> value from the resource provided in the request
321                 *      bundle entry corresponding to this outcome.
322                 */
323                public String getRequestMetaSource() {
324                        return myRequestMetaSource;
325                }
326        }
327}