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