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