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        public static IBase addIssueWithMessageId(
268                        FhirContext myCtx,
269                        IBaseOperationOutcome theOperationOutcome,
270                        String theSeverity,
271                        String theMessage,
272                        String theMessageId,
273                        String theLocation,
274                        String theCode) {
275                IBase issue = addIssue(myCtx, theOperationOutcome, theSeverity, theMessage, theLocation, theCode);
276                if (isNotBlank(theMessageId)) {
277                        addDetailsToIssue(myCtx, issue, Constants.JAVA_VALIDATOR_DETAILS_SYSTEM, theMessageId);
278                }
279
280                return issue;
281        }
282
283        public static void addDetailsToIssue(FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode) {
284                addDetailsToIssue(theFhirContext, theIssue, theSystem, theCode, null);
285        }
286
287        public static void addDetailsToIssue(
288                        FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode, String theText) {
289                BaseRuntimeElementCompositeDefinition<?> issueElement =
290                                (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(theIssue.getClass());
291                BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details");
292                BaseRuntimeElementCompositeDefinition<?> ccDef =
293                                (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("CodeableConcept");
294                ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance();
295
296                if (isNotBlank(theSystem) || isNotBlank(theCode)) {
297                        BaseRuntimeElementCompositeDefinition<?> codingDef =
298                                        (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("Coding");
299                        ICompositeType coding = (ICompositeType) codingDef.newInstance();
300
301                        // System
302                        if (isNotBlank(theSystem)) {
303                                IPrimitiveType<?> system = (IPrimitiveType<?>)
304                                                theFhirContext.getElementDefinition("uri").newInstance();
305                                system.setValueAsString(theSystem);
306                                codingDef.getChildByName("system").getMutator().addValue(coding, system);
307                        }
308
309                        // Code
310                        if (isNotBlank(theCode)) {
311                                IPrimitiveType<?> code = (IPrimitiveType<?>)
312                                                theFhirContext.getElementDefinition("code").newInstance();
313                                code.setValueAsString(theCode);
314                                codingDef.getChildByName("code").getMutator().addValue(coding, code);
315                        }
316
317                        ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding);
318                }
319
320                if (isNotBlank(theText)) {
321                        IPrimitiveType<?> textElem = (IPrimitiveType<?>)
322                                        ccDef.getChildByName("text").getChildByName("text").newInstance(theText);
323                        ccDef.getChildByName("text").getMutator().addValue(codeableConcept, textElem);
324                }
325
326                detailsChildDef.getMutator().addValue(theIssue, codeableConcept);
327        }
328
329        public static void addIssueLineExtensionToIssue(FhirContext theCtx, IBase theIssue, String theLine) {
330                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
331                        ExtensionUtil.setExtension(
332                                        theCtx,
333                                        theIssue,
334                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line",
335                                        "integer",
336                                        theLine);
337                }
338        }
339
340        public static void addIssueColExtensionToIssue(FhirContext theCtx, IBase theIssue, String theColumn) {
341                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
342                        ExtensionUtil.setExtension(
343                                        theCtx,
344                                        theIssue,
345                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col",
346                                        "integer",
347                                        theColumn);
348                }
349        }
350
351        public static void addMessageIdExtensionToIssue(FhirContext theCtx, IBase theIssue, String theMessageId) {
352                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
353                        ExtensionUtil.setExtension(
354                                        theCtx,
355                                        theIssue,
356                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id",
357                                        "string",
358                                        theMessageId);
359                }
360        }
361}