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}