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