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.model.api.StorageResponseCodeEnum;
024import ca.uhn.fhir.model.dstu2.valueset.IssueTypeEnum;
025import ca.uhn.fhir.util.FhirTerser;
026import org.hl7.fhir.instance.model.api.IBase;
027import org.hl7.fhir.instance.model.api.IBaseBundle;
028import org.hl7.fhir.instance.model.api.IIdType;
029
030import java.util.ArrayList;
031import java.util.List;
032
033/**
034 * This class contains utility methods for working with HAPI FHIR Transactions (referring to the FHIR
035 * "transaction" operation, as opposed to working with database transactions).
036 */
037public class TransactionUtil {
038
039        /**
040         * Non instantiable
041         */
042        private TransactionUtil() {
043                super();
044        }
045
046        /**
047         * This method accepts a {@literal Bundle} which was returned by a call to HAPI FHIR's
048         * transaction/batch processor. HAPI FHIR has specific codes and extensions it will
049         * always put into the OperationOutcomes returned by the transaction processor, so
050         * this method parses these and returns a more machine-processable interpretation.
051         * <p>
052         * This method should only be called for Bundles returned by HAPI FHIR's transaction
053         * processor, results will have no meaning for any other input.
054         * </p>
055         *
056         * @since 8.2.0
057         */
058        public static TransactionResponse parseTransactionResponse(
059                        FhirContext theContext, IBaseBundle theTransactionResponseBundle) {
060                FhirTerser terser = theContext.newTerser();
061                List<StorageOutcome> storageOutcomes = new ArrayList<>();
062
063                List<IBase> entries = terser.getValues(theTransactionResponseBundle, "entry");
064                for (IBase entry : entries) {
065
066                        IBase response = terser.getSingleValueOrNull(entry, "response", IBase.class);
067                        if (response != null) {
068
069                                // As long as we're parsing a bundle from HAPI, this will never be used. But let's be
070                                // defensive just in case.
071                                int statusCode = 0;
072                                String statusString = terser.getSinglePrimitiveValueOrNull(response, "status");
073                                if (statusString != null) {
074                                        int statusSpaceIdx = statusString.indexOf(' ');
075                                        statusCode = Integer.parseInt(statusString.substring(0, statusSpaceIdx));
076                                }
077
078                                List<IBase> issues = terser.getValues(response, "outcome.issue");
079                                IIdType groupSourceId = null;
080                                for (int issueIndex = 0; issueIndex < issues.size(); issueIndex++) {
081                                        IBase issue = issues.get(issueIndex);
082                                        IIdType sourceId = null;
083
084                                        String outcomeSystem = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.system");
085                                        StorageResponseCodeEnum responseCode = null;
086                                        if (StorageResponseCodeEnum.SYSTEM.equals(outcomeSystem)) {
087                                                String outcomeCode = terser.getSinglePrimitiveValueOrNull(issue, "details.coding.code");
088                                                responseCode = StorageResponseCodeEnum.valueOf(outcomeCode);
089                                        }
090
091                                        String errorMessage = null;
092                                        String issueCode = terser.getSinglePrimitiveValueOrNull(issue, "code");
093                                        if (IssueTypeEnum.EXCEPTION.getCode().equals(issueCode)) {
094                                                errorMessage = terser.getSinglePrimitiveValueOrNull(issue, "diagnostics");
095                                        }
096
097                                        IIdType targetId = null;
098                                        if (responseCode == StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE) {
099                                                /*
100                                                 * The first issue on a transaction response OO will have the details about the
101                                                 * processing of the actual input resource that was in the input transaction bundle.
102                                                 * However, if any automatically created placeholders were created during the
103                                                 * processing of that resource, details about those will be placed in subsequent
104                                                 * issues.
105                                                 */
106
107                                                /*
108                                                TODO: uncomment this when branch ja_20250217_tx_log_provenance merges
109                                                targetId = ((IBaseHasExtensions) issue)
110                                                        .getExtension().stream()
111                                                        .filter(t -> HapiExtensions.EXTENSION_PLACEHOLDER_ID.equals(t.getUrl()))
112                                                        .findFirst()
113                                                        .map(t -> (IIdType) t.getValue())
114                                                        .orElse(null);
115                                                */
116                                                sourceId = groupSourceId;
117                                        } else {
118                                                targetId = theContext
119                                                                .getVersion()
120                                                                .newIdType(terser.getSinglePrimitiveValueOrNull(entry, "response.location"));
121                                                if (issueIndex == 0) {
122                                                        groupSourceId = targetId;
123                                                }
124                                        }
125
126                                        StorageOutcome outcome =
127                                                        new StorageOutcome(statusCode, responseCode, targetId, sourceId, errorMessage);
128                                        storageOutcomes.add(outcome);
129                                }
130                        }
131                }
132
133                return new TransactionResponse(storageOutcomes);
134        }
135
136        /**
137         * @see #parseTransactionResponse(FhirContext, IBaseBundle)
138         */
139        public static class TransactionResponse {
140
141                private final List<StorageOutcome> myStorageOutcomes;
142
143                public TransactionResponse(List<StorageOutcome> theStorageOutcomes) {
144                        myStorageOutcomes = theStorageOutcomes;
145                }
146
147                public List<StorageOutcome> getStorageOutcomes() {
148                        return myStorageOutcomes;
149                }
150        }
151
152        /**
153         * @see #parseTransactionResponse(FhirContext, IBaseBundle)
154         */
155        public static class StorageOutcome {
156                private final StorageResponseCodeEnum myStorageResponseCode;
157                private final IIdType myTargetId;
158                private final IIdType mySourceId;
159                private final int myStatusCode;
160                private final String myErrorMessage;
161
162                public StorageOutcome(
163                                int theStatusCode,
164                                StorageResponseCodeEnum theStorageResponseCode,
165                                IIdType theTargetId,
166                                IIdType theSourceId,
167                                String theErrorMessage) {
168                        myStatusCode = theStatusCode;
169                        myStorageResponseCode = theStorageResponseCode;
170                        myTargetId = theTargetId;
171                        mySourceId = theSourceId;
172                        myErrorMessage = theErrorMessage;
173                }
174
175                public String getErrorMessage() {
176                        return myErrorMessage;
177                }
178
179                public int getStatusCode() {
180                        return myStatusCode;
181                }
182
183                public StorageResponseCodeEnum getStorageResponseCode() {
184                        return myStorageResponseCode;
185                }
186
187                public IIdType getTargetId() {
188                        return myTargetId;
189                }
190
191                public IIdType getSourceId() {
192                        return mySourceId;
193                }
194        }
195}