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.size() > i ? requestEntries.get(i) : null;
097                        IBase responseEntry = responseEntries.get(i);
098
099                        IBaseResource requestResource = null;
100                        IIdType requestFullUrl = null;
101                        if (requestEntry != null) {
102                                String requestVerb = terser.getSinglePrimitiveValueOrNull(requestEntry, "request.method");
103                                if ("GET".equals(requestVerb)) {
104                                        continue;
105                                }
106
107                                requestResource = terser.getSingleValueOrNull(requestEntry, "resource", IBaseResource.class);
108
109                                String requestFullUrlString = terser.getSinglePrimitiveValueOrNull(requestEntry, "fullUrl");
110                                if (requestFullUrlString != null) {
111                                        requestFullUrl = theContext.getVersion().newIdType(requestFullUrlString);
112                                        requestFullUrl = toUnqualified(requestFullUrl);
113                                }
114                        }
115
116                        String requestMetaSource = null;
117                        if (requestResource != null
118                                        && theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
119                                requestMetaSource = MetaUtil.getSource(theContext, requestResource);
120                        }
121                        if (isBlank(requestMetaSource)) {
122                                requestMetaSource = bundleMetaSource;
123                        }
124
125                        IBase responseResponse = terser.getSingleValueOrNull(responseEntry, "response", IBase.class);
126                        if (responseResponse != null) {
127
128                                // As long as we're parsing a bundle from HAPI, this will never be used. But let's be
129                                // defensive just in case.
130                                int statusCode = 0;
131                                String statusMessage = terser.getSinglePrimitiveValueOrNull(responseResponse, "status");
132                                if (statusMessage != null) {
133                                        int statusSpaceIdx = statusMessage.indexOf(' ');
134                                        if (statusSpaceIdx > 0) {
135                                                statusCode = Integer.parseInt(statusMessage.substring(0, statusSpaceIdx));
136                                        }
137                                }
138
139                                List<IBase> issues = Collections.emptyList();
140                                if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
141                                        issues = terser.getValues(responseResponse, "outcome.issue");
142                                } else {
143                                        IBaseResource responseResource =
144                                                        terser.getSingleValueOrNull(responseEntry, "resource", IBaseResource.class);
145                                        if (responseResource instanceof IBaseOperationOutcome) {
146                                                issues = terser.getValues(responseResource, "issue");
147                                        }
148                                }
149                                IIdType groupSourceId = null;
150                                for (int issueIndex = 0; issueIndex < issues.size(); issueIndex++) {
151                                        IBase issue = issues.get(issueIndex);
152                                        IIdType sourceId = requestFullUrl;
153
154                                        String outcomeSystem = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.system");
155                                        StorageResponseCodeEnum responseCode = null;
156                                        if (StorageResponseCodeEnum.SYSTEM.equals(outcomeSystem)) {
157                                                String outcomeCode = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.code");
158                                                responseCode = StorageResponseCodeEnum.valueOf(outcomeCode);
159                                        }
160
161                                        String errorMessage = null;
162                                        String issueSeverityString = terser.getSinglePrimitiveValueOrNull(issue, "severity");
163                                        if (isNotBlank(issueSeverityString)) {
164                                                IValidationSupport.IssueSeverity issueSeverity =
165                                                                IValidationSupport.IssueSeverity.fromCode(issueSeverityString);
166                                                if (issueSeverity != null) {
167                                                        if (issueSeverity.ordinal() <= IValidationSupport.IssueSeverity.ERROR.ordinal()) {
168                                                                errorMessage = terser.getSinglePrimitiveValueOrNull(issue, "diagnostics");
169                                                        }
170                                                }
171                                        }
172
173                                        if (responseCode == null && statusCode >= 400 && statusCode <= 599) {
174                                                responseCode = StorageResponseCodeEnum.FAILURE;
175                                        }
176
177                                        IIdType targetId;
178                                        if (responseCode == StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE) {
179                                                /*
180                                                 * The first issue on a transaction response OO will have the details about the
181                                                 * processing of the actual input resource that was in the input transaction bundle.
182                                                 * However, if any automatically created placeholders were created during the
183                                                 * processing of that resource, details about those will be placed in subsequent
184                                                 * issues.
185                                                 */
186
187                                                targetId = ((IBaseHasExtensions) issue)
188                                                                .getExtension().stream()
189                                                                                .filter(t -> HapiExtensions.EXTENSION_PLACEHOLDER_ID.equals(t.getUrl()))
190                                                                                .findFirst()
191                                                                                .map(t -> (IIdType) t.getValue())
192                                                                                .orElse(null);
193                                                sourceId = groupSourceId;
194                                        } else {
195                                                String responseLocation =
196                                                                terser.getSinglePrimitiveValueOrNull(responseEntry, "response.location");
197                                                if (isNotBlank(responseLocation)) {
198                                                        targetId = theContext.getVersion().newIdType(responseLocation);
199                                                        if (issueIndex == 0) {
200                                                                groupSourceId = targetId;
201                                                        }
202                                                } else {
203                                                        targetId = null;
204                                                }
205                                        }
206
207                                        StorageOutcome outcome = new StorageOutcome(
208                                                        statusCode,
209                                                        statusMessage,
210                                                        responseCode,
211                                                        toUnqualified(sourceId),
212                                                        toUnqualified(targetId),
213                                                        errorMessage,
214                                                        requestMetaSource);
215                                        storageOutcomes.add(outcome);
216                                }
217                        }
218                }
219
220                return new TransactionResponse(storageOutcomes);
221        }
222
223        private static IIdType toUnqualified(@Nullable IIdType theId) {
224                if (theId != null && theId.hasBaseUrl()) {
225                        return theId.toUnqualified();
226                }
227                return theId;
228        }
229
230        /**
231         * @see #parseTransactionResponse(FhirContext, IBaseBundle, IBaseBundle)
232         */
233        public static class TransactionResponse {
234
235                private final List<StorageOutcome> myStorageOutcomes;
236
237                public TransactionResponse(List<StorageOutcome> theStorageOutcomes) {
238                        myStorageOutcomes = theStorageOutcomes;
239                }
240
241                public List<StorageOutcome> getStorageOutcomes() {
242                        return myStorageOutcomes;
243                }
244        }
245
246        /**
247         * @see #parseTransactionResponse(FhirContext, IBaseBundle, IBaseBundle)
248         */
249        public static class StorageOutcome {
250                private final StorageResponseCodeEnum myStorageResponseCode;
251                private final IIdType myTargetId;
252                private final IIdType mySourceId;
253                private final int myStatusCode;
254                private final String myErrorMessage;
255                private final String myRequestMetaSource;
256                private final String myStatusMessage;
257
258                public StorageOutcome(
259                                int theStatusCode,
260                                String theStatusMessage,
261                                StorageResponseCodeEnum theStorageResponseCode,
262                                IIdType theSourceId,
263                                IIdType theTargetId,
264                                String theErrorMessage,
265                                String theRequestMetaSource) {
266                        myStatusCode = theStatusCode;
267                        myStatusMessage = theStatusMessage;
268                        myStorageResponseCode = theStorageResponseCode;
269                        myTargetId = theTargetId;
270                        mySourceId = theSourceId;
271                        myErrorMessage = theErrorMessage;
272                        myRequestMetaSource = theRequestMetaSource;
273                }
274
275                /**
276                 * @return Returns an error message if the specific action resulted in a failure. Returns {@literal null}
277                 *      otherwise.
278                 */
279                public String getErrorMessage() {
280                        return myErrorMessage;
281                }
282
283                /**
284                 * @return Returns the HTTP status code
285                 */
286                public int getStatusCode() {
287                        return myStatusCode;
288                }
289
290                /**
291                 * @return Returns the complete HTTP status message including the {@link #getStatusCode()} and the rest of the message. For example: {@literal 200 OK}
292                 */
293                public String getStatusMessage() {
294                        return myStatusMessage;
295                }
296
297                /**
298                 * @return Contains a code identifying the specific outcome of this operation.
299                 */
300                public StorageResponseCodeEnum getStorageResponseCode() {
301                        return myStorageResponseCode;
302                }
303
304                /**
305                 * @return Returns the ID of the resource as it was stored in the repository.
306                 */
307                public IIdType getTargetId() {
308                        return myTargetId;
309                }
310
311                /**
312                 * @return Returns the ID of the resource in the request bundle in most cases. This could be an actual
313                 *      resource ID if the operation was an update by ID, or a placeholder UUID if placeholder IDs were in
314                 *      use in the bundle. If the {@link #getStorageResponseCode()} for this outcome is
315                 *    {@link StorageResponseCodeEnum#AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE}, the source ID will be the
316                 *      actual resolved and stored resource ID of the resource containing the reference which caused the
317                 *      placeholder to be created. The ID returned will be unqualified, meaning it has no base URL.
318                 */
319                public IIdType getSourceId() {
320                        return mySourceId;
321                }
322
323                /**
324                 * @return Returns the <code>Resource.meta.source</code> value from the resource provided in the request
325                 *      bundle entry corresponding to this outcome.
326                 */
327                public String getRequestMetaSource() {
328                        return myRequestMetaSource;
329                }
330        }
331}