
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}