
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}