
001/* 002 * #%L 003 * HAPI FHIR - Core Library 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.util; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.RuntimeResourceDefinition; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.model.api.StorageResponseCodeEnum; 030import ca.uhn.fhir.rest.api.Constants; 031import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 032import jakarta.annotation.Nullable; 033import org.hl7.fhir.instance.model.api.IBase; 034import org.hl7.fhir.instance.model.api.IBaseCoding; 035import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037import org.hl7.fhir.instance.model.api.ICompositeType; 038import org.hl7.fhir.instance.model.api.IPrimitiveType; 039 040import java.util.List; 041 042import static org.apache.commons.lang3.StringUtils.isNotBlank; 043 044/** 045 * Utilities for dealing with OperationOutcome resources across various model versions 046 */ 047public class OperationOutcomeUtil { 048 049 public static final String OO_SEVERITY_ERROR = "error"; 050 public static final String OO_SEVERITY_INFO = "information"; 051 public static final String OO_SEVERITY_WARN = "warning"; 052 public static final String OO_ISSUE_CODE_INFORMATIONAL = "informational"; 053 054 /** 055 * Note: This code was added in FHIR R5, so the {@link #addIssue(FhirContext, IBaseOperationOutcome, String, String, String, String) addIssue} 056 * methods here will automatically convert it to {@link #OO_ISSUE_CODE_INFORMATIONAL} for 057 * previous versions of FHIR. 058 * 059 * @since 8.6.0 060 */ 061 public static final String OO_ISSUE_CODE_SUCCESS = "success"; 062 063 /** 064 * @since 8.6.0 065 */ 066 public static final String OO_ISSUE_CODE_PROCESSING = "processing"; 067 068 /** 069 * Add an issue to an OperationOutcome 070 * 071 * @param theCtx The fhir context 072 * @param theOperationOutcome The OO resource to add to 073 * @param theSeverity The severity (fatal | error | warning | information) 074 * @param theDiagnostics The diagnostics string (this was called "details" in FHIR DSTU2 but was renamed to diagnostics in DSTU3) 075 * @param theCode A code, such as {@link #OO_ISSUE_CODE_INFORMATIONAL} or {@link #OO_ISSUE_CODE_SUCCESS} 076 * @return Returns the newly added issue 077 */ 078 public static IBase addIssue( 079 FhirContext theCtx, 080 IBaseOperationOutcome theOperationOutcome, 081 String theSeverity, 082 String theDiagnostics, 083 String theLocation, 084 String theCode) { 085 return addIssue( 086 theCtx, theOperationOutcome, theSeverity, theDiagnostics, theLocation, theCode, null, null, null); 087 } 088 089 public static IBase addIssue( 090 FhirContext theCtx, 091 IBaseOperationOutcome theOperationOutcome, 092 String theSeverity, 093 String theDiagnostics, 094 String theLocation, 095 String theCode, 096 @Nullable String theDetailSystem, 097 @Nullable String theDetailCode, 098 @Nullable String theDetailDescription) { 099 IBase issue = createIssue(theCtx, theOperationOutcome); 100 populateDetails( 101 theCtx, 102 issue, 103 theSeverity, 104 theDiagnostics, 105 theLocation, 106 theCode, 107 theDetailSystem, 108 theDetailCode, 109 theDetailDescription); 110 return issue; 111 } 112 113 private static IBase createIssue(FhirContext theCtx, IBaseResource theOutcome) { 114 RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); 115 BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); 116 BaseRuntimeElementCompositeDefinition<?> issueElement = 117 (BaseRuntimeElementCompositeDefinition<?>) issueChild.getChildByName("issue"); 118 119 IBase issue = issueElement.newInstance(); 120 issueChild.getMutator().addValue(theOutcome, issue); 121 return issue; 122 } 123 124 /** 125 * @deprecated Use {@link #getFirstIssueDiagnostics(FhirContext, IBaseOperationOutcome)} instead. This 126 * method has always been misnamed for historical reasons. 127 */ 128 @Deprecated(forRemoval = true, since = "8.2.0") 129 public static String getFirstIssueDetails(FhirContext theCtx, IBaseOperationOutcome theOutcome) { 130 return getFirstIssueDiagnostics(theCtx, theOutcome); 131 } 132 133 public static String getFirstIssueDiagnostics(FhirContext theCtx, IBaseOperationOutcome theOutcome) { 134 return getIssueStringPart(theCtx, theOutcome, "diagnostics", 0); 135 } 136 137 public static String getIssueDiagnostics(FhirContext theCtx, IBaseOperationOutcome theOutcome, int theIndex) { 138 return getIssueStringPart(theCtx, theOutcome, "diagnostics", theIndex); 139 } 140 141 public static String getFirstIssueLocation(FhirContext theCtx, IBaseOperationOutcome theOutcome) { 142 return getIssueStringPart(theCtx, theOutcome, "location", 0); 143 } 144 145 private static String getIssueStringPart( 146 FhirContext theCtx, IBaseOperationOutcome theOutcome, String theName, int theIndex) { 147 if (theOutcome == null) { 148 return null; 149 } 150 151 RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); 152 BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); 153 154 List<IBase> issues = issueChild.getAccessor().getValues(theOutcome); 155 if (issues.size() <= theIndex) { 156 return null; 157 } 158 159 IBase issue = issues.get(theIndex); 160 BaseRuntimeElementCompositeDefinition<?> issueElement = 161 (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(issue.getClass()); 162 BaseRuntimeChildDefinition detailsChild = issueElement.getChildByName(theName); 163 164 List<IBase> details = detailsChild.getAccessor().getValues(issue); 165 if (details.isEmpty()) { 166 return null; 167 } 168 return ((IPrimitiveType<?>) details.get(0)).getValueAsString(); 169 } 170 171 /** 172 * Returns true if the given OperationOutcome has 1 or more Operation.issue repetitions 173 */ 174 public static boolean hasIssues(FhirContext theCtx, IBaseOperationOutcome theOutcome) { 175 if (theOutcome == null) { 176 return false; 177 } 178 return getIssueCount(theCtx, theOutcome) > 0; 179 } 180 181 public static int getIssueCount(FhirContext theCtx, IBaseOperationOutcome theOutcome) { 182 RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); 183 BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); 184 return issueChild.getAccessor().getValues(theOutcome).size(); 185 } 186 187 public static boolean hasIssuesOfSeverity( 188 FhirContext theCtx, IBaseOperationOutcome theOutcome, String theSeverity) { 189 RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); 190 BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); 191 List<IBase> issues = issueChild.getAccessor().getValues(theOutcome); 192 193 if (issues.isEmpty()) { 194 return false; // if there are no issues at all, there are no issues of the required severity 195 } 196 197 IBase firstIssue = issues.get(0); 198 BaseRuntimeElementCompositeDefinition<?> issueElement = 199 (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(firstIssue.getClass()); 200 BaseRuntimeChildDefinition severityChild = issueElement.getChildByName("severity"); 201 202 return issues.stream() 203 .flatMap(t -> severityChild.getAccessor().getValues(t).stream()) 204 .map(t -> (IPrimitiveType<?>) t) 205 .map(IPrimitiveType::getValueAsString) 206 .anyMatch(theSeverity::equals); 207 } 208 209 public static IBaseOperationOutcome newInstance(FhirContext theCtx) { 210 RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition("OperationOutcome"); 211 try { 212 return (IBaseOperationOutcome) ooDef.getImplementingClass().newInstance(); 213 } catch (InstantiationException e) { 214 throw new InternalErrorException(Msg.code(1803) + "Unable to instantiate OperationOutcome", e); 215 } catch (IllegalAccessException e) { 216 throw new InternalErrorException(Msg.code(1804) + "Unable to instantiate OperationOutcome", e); 217 } 218 } 219 220 private static void populateDetails( 221 FhirContext theCtx, 222 IBase theIssue, 223 String theSeverity, 224 String theDiagnostics, 225 String theLocation, 226 String theCode, 227 String theDetailSystem, 228 String theDetailCode, 229 String theDetailDescription) { 230 BaseRuntimeElementCompositeDefinition<?> issueElement = 231 (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(theIssue.getClass()); 232 BaseRuntimeChildDefinition diagnosticsChild; 233 diagnosticsChild = issueElement.getChildByName("diagnostics"); 234 235 BaseRuntimeChildDefinition codeChild = issueElement.getChildByName("code"); 236 IPrimitiveType<?> codeElem = (IPrimitiveType<?>) 237 codeChild.getChildByName("code").newInstance(codeChild.getInstanceConstructorArguments()); 238 String code = theCode; 239 if (theCtx.getVersion().getVersion().isOlderThan(FhirVersionEnum.R5) && "success".equals(code)) { 240 // "success" was added in R5 so we switch back to "informational" for older versions 241 code = "informational"; 242 } 243 244 codeElem.setValueAsString(code); 245 codeChild.getMutator().addValue(theIssue, codeElem); 246 247 BaseRuntimeElementDefinition<?> stringDef = diagnosticsChild.getChildByName(diagnosticsChild.getElementName()); 248 BaseRuntimeChildDefinition severityChild = issueElement.getChildByName("severity"); 249 250 IPrimitiveType<?> severityElem = (IPrimitiveType<?>) 251 severityChild.getChildByName("severity").newInstance(severityChild.getInstanceConstructorArguments()); 252 severityElem.setValueAsString(theSeverity); 253 severityChild.getMutator().addValue(theIssue, severityElem); 254 255 IPrimitiveType<?> string = (IPrimitiveType<?>) stringDef.newInstance(); 256 string.setValueAsString(theDiagnostics); 257 diagnosticsChild.getMutator().setValue(theIssue, string); 258 259 addLocationToIssue(theCtx, theIssue, theLocation); 260 261 if (isNotBlank(theDetailSystem)) { 262 BaseRuntimeChildDefinition detailsChild = issueElement.getChildByName("details"); 263 if (detailsChild != null) { 264 BaseRuntimeElementDefinition<?> codeableConceptDef = theCtx.getElementDefinition("CodeableConcept"); 265 IBase codeableConcept = codeableConceptDef.newInstance(); 266 267 BaseRuntimeElementDefinition<?> codingDef = theCtx.getElementDefinition("Coding"); 268 IBaseCoding coding = (IBaseCoding) codingDef.newInstance(); 269 coding.setSystem(theDetailSystem); 270 coding.setCode(theDetailCode); 271 coding.setDisplay(theDetailDescription); 272 273 codeableConceptDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); 274 275 detailsChild.getMutator().addValue(theIssue, codeableConcept); 276 } 277 } 278 } 279 280 public static void addLocationToIssue(FhirContext theContext, IBase theIssue, String theLocation) { 281 if (isNotBlank(theLocation)) { 282 BaseRuntimeElementCompositeDefinition<?> issueElement = 283 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theIssue.getClass()); 284 BaseRuntimeChildDefinition locationChild = issueElement.getChildByName("location"); 285 IPrimitiveType<?> locationElem = (IPrimitiveType<?>) locationChild 286 .getChildByName("location") 287 .newInstance(locationChild.getInstanceConstructorArguments()); 288 locationElem.setValueAsString(theLocation); 289 locationChild.getMutator().addValue(theIssue, locationElem); 290 } 291 } 292 293 /** 294 * Given an instance of <code>OperationOutcome.issue</code>, adds a new instance of 295 * <code>OperationOutcome.issue.expression</code> with the given string value. 296 * 297 * @param theContext The FhirContext for the appropriate FHIR version 298 * @param theIssue The <code>OperationOutcome.issue</code> to add to 299 * @param theLocationExpression The string to use as content 300 */ 301 public static void addExpressionToIssue(FhirContext theContext, IBase theIssue, String theLocationExpression) { 302 if (isNotBlank(theLocationExpression) 303 && theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { 304 BaseRuntimeElementCompositeDefinition<?> issueElement = 305 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theIssue.getClass()); 306 BaseRuntimeChildDefinition locationChild = issueElement.getChildByName("expression"); 307 IPrimitiveType<?> locationElem = (IPrimitiveType<?>) locationChild 308 .getChildByName("expression") 309 .newInstance(locationChild.getInstanceConstructorArguments()); 310 locationElem.setValueAsString(theLocationExpression); 311 locationChild.getMutator().addValue(theIssue, locationElem); 312 } 313 } 314 315 public static IBase addIssueWithMessageId( 316 FhirContext myCtx, 317 IBaseOperationOutcome theOperationOutcome, 318 String theSeverity, 319 String theMessage, 320 String theMessageId, 321 String theLocation, 322 String theCode) { 323 IBase issue = addIssue(myCtx, theOperationOutcome, theSeverity, theMessage, theLocation, theCode); 324 if (isNotBlank(theMessageId)) { 325 addDetailsToIssue(myCtx, issue, Constants.JAVA_VALIDATOR_DETAILS_SYSTEM, theMessageId); 326 } 327 328 return issue; 329 } 330 331 public static void addDetailsToIssue(FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode) { 332 addDetailsToIssue(theFhirContext, theIssue, theSystem, theCode, null); 333 } 334 335 public static void addDetailsToIssue( 336 FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode, String theText) { 337 BaseRuntimeElementCompositeDefinition<?> issueElement = 338 (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(theIssue.getClass()); 339 BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details"); 340 BaseRuntimeElementCompositeDefinition<?> ccDef = 341 (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("CodeableConcept"); 342 ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance(); 343 344 if (isNotBlank(theSystem) || isNotBlank(theCode)) { 345 BaseRuntimeElementCompositeDefinition<?> codingDef = 346 (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("Coding"); 347 ICompositeType coding = (ICompositeType) codingDef.newInstance(); 348 349 // System 350 if (isNotBlank(theSystem)) { 351 IPrimitiveType<?> system = (IPrimitiveType<?>) 352 theFhirContext.getElementDefinition("uri").newInstance(); 353 system.setValueAsString(theSystem); 354 codingDef.getChildByName("system").getMutator().addValue(coding, system); 355 } 356 357 // Code 358 if (isNotBlank(theCode)) { 359 IPrimitiveType<?> code = (IPrimitiveType<?>) 360 theFhirContext.getElementDefinition("code").newInstance(); 361 code.setValueAsString(theCode); 362 codingDef.getChildByName("code").getMutator().addValue(coding, code); 363 } 364 365 ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); 366 } 367 368 if (isNotBlank(theText)) { 369 IPrimitiveType<?> textElem = (IPrimitiveType<?>) 370 ccDef.getChildByName("text").getChildByName("text").newInstance(theText); 371 ccDef.getChildByName("text").getMutator().addValue(codeableConcept, textElem); 372 } 373 374 detailsChildDef.getMutator().addValue(theIssue, codeableConcept); 375 } 376 377 public static void addIssueLineExtensionToIssue(FhirContext theCtx, IBase theIssue, String theLine) { 378 if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { 379 ExtensionUtil.setExtension( 380 theCtx, 381 theIssue, 382 "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line", 383 "integer", 384 theLine); 385 } 386 } 387 388 public static void addIssueColExtensionToIssue(FhirContext theCtx, IBase theIssue, String theColumn) { 389 if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { 390 ExtensionUtil.setExtension( 391 theCtx, 392 theIssue, 393 "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col", 394 "integer", 395 theColumn); 396 } 397 } 398 399 public static void addMessageIdExtensionToIssue(FhirContext theCtx, IBase theIssue, String theMessageId) { 400 if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) { 401 ExtensionUtil.setExtension( 402 theCtx, 403 theIssue, 404 "http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id", 405 "string", 406 theMessageId); 407 } 408 } 409 410 public static IBaseOperationOutcome createOperationOutcome( 411 String theSeverity, 412 String theMessage, 413 String theCode, 414 FhirContext theFhirContext, 415 @Nullable StorageResponseCodeEnum theStorageResponseCode) { 416 IBaseOperationOutcome oo = newInstance(theFhirContext); 417 String detailSystem = null; 418 String detailCode = null; 419 String detailDescription = null; 420 if (theStorageResponseCode != null) { 421 detailSystem = theStorageResponseCode.getSystem(); 422 detailCode = theStorageResponseCode.getCode(); 423 detailDescription = theStorageResponseCode.getDisplay(); 424 } 425 addIssue( 426 theFhirContext, 427 oo, 428 theSeverity, 429 theMessage, 430 null, 431 theCode, 432 detailSystem, 433 detailCode, 434 detailDescription); 435 return oo; 436 } 437}