
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}