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