001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 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        public static String getFirstIssueDetails(FhirContext theCtx, IBaseOperationOutcome theOutcome) {
103                return getFirstIssueStringPart(theCtx, theOutcome, "diagnostics");
104        }
105
106        public static String getFirstIssueLocation(FhirContext theCtx, IBaseOperationOutcome theOutcome) {
107                return getFirstIssueStringPart(theCtx, theOutcome, "location");
108        }
109
110        private static String getFirstIssueStringPart(FhirContext theCtx, IBaseOperationOutcome theOutcome, String name) {
111                if (theOutcome == null) {
112                        return null;
113                }
114
115                RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome);
116                BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue");
117
118                List<IBase> issues = issueChild.getAccessor().getValues(theOutcome);
119                if (issues.isEmpty()) {
120                        return null;
121                }
122
123                IBase issue = issues.get(0);
124                BaseRuntimeElementCompositeDefinition<?> issueElement =
125                                (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(issue.getClass());
126                BaseRuntimeChildDefinition detailsChild = issueElement.getChildByName(name);
127
128                List<IBase> details = detailsChild.getAccessor().getValues(issue);
129                if (details.isEmpty()) {
130                        return null;
131                }
132                return ((IPrimitiveType<?>) details.get(0)).getValueAsString();
133        }
134
135        /**
136         * Returns true if the given OperationOutcome has 1 or more Operation.issue repetitions
137         */
138        public static boolean hasIssues(FhirContext theCtx, IBaseOperationOutcome theOutcome) {
139                if (theOutcome == null) {
140                        return false;
141                }
142                return getIssueCount(theCtx, theOutcome) > 0;
143        }
144
145        public static int getIssueCount(FhirContext theCtx, IBaseOperationOutcome theOutcome) {
146                RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome);
147                BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue");
148                return issueChild.getAccessor().getValues(theOutcome).size();
149        }
150
151        public static boolean hasIssuesOfSeverity(
152                        FhirContext theCtx, IBaseOperationOutcome theOutcome, String theSeverity) {
153                RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome);
154                BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue");
155                List<IBase> issues = issueChild.getAccessor().getValues(theOutcome);
156
157                if (issues.isEmpty()) {
158                        return false; // if there are no issues at all, there are no issues of the required severity
159                }
160
161                IBase firstIssue = issues.get(0);
162                BaseRuntimeElementCompositeDefinition<?> issueElement =
163                                (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(firstIssue.getClass());
164                BaseRuntimeChildDefinition severityChild = issueElement.getChildByName("severity");
165
166                return issues.stream()
167                                .flatMap(t -> severityChild.getAccessor().getValues(t).stream())
168                                .map(t -> (IPrimitiveType<?>) t)
169                                .map(IPrimitiveType::getValueAsString)
170                                .anyMatch(theSeverity::equals);
171        }
172
173        public static IBaseOperationOutcome newInstance(FhirContext theCtx) {
174                RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition("OperationOutcome");
175                try {
176                        return (IBaseOperationOutcome) ooDef.getImplementingClass().newInstance();
177                } catch (InstantiationException e) {
178                        throw new InternalErrorException(Msg.code(1803) + "Unable to instantiate OperationOutcome", e);
179                } catch (IllegalAccessException e) {
180                        throw new InternalErrorException(Msg.code(1804) + "Unable to instantiate OperationOutcome", e);
181                }
182        }
183
184        private static void populateDetails(
185                        FhirContext theCtx,
186                        IBase theIssue,
187                        String theSeverity,
188                        String theDetails,
189                        String theLocation,
190                        String theCode,
191                        String theDetailSystem,
192                        String theDetailCode,
193                        String theDetailDescription) {
194                BaseRuntimeElementCompositeDefinition<?> issueElement =
195                                (BaseRuntimeElementCompositeDefinition<?>) theCtx.getElementDefinition(theIssue.getClass());
196                BaseRuntimeChildDefinition diagnosticsChild;
197                diagnosticsChild = issueElement.getChildByName("diagnostics");
198
199                BaseRuntimeChildDefinition codeChild = issueElement.getChildByName("code");
200                IPrimitiveType<?> codeElem = (IPrimitiveType<?>)
201                                codeChild.getChildByName("code").newInstance(codeChild.getInstanceConstructorArguments());
202                codeElem.setValueAsString(theCode);
203                codeChild.getMutator().addValue(theIssue, codeElem);
204
205                BaseRuntimeElementDefinition<?> stringDef = diagnosticsChild.getChildByName(diagnosticsChild.getElementName());
206                BaseRuntimeChildDefinition severityChild = issueElement.getChildByName("severity");
207
208                IPrimitiveType<?> severityElem = (IPrimitiveType<?>)
209                                severityChild.getChildByName("severity").newInstance(severityChild.getInstanceConstructorArguments());
210                severityElem.setValueAsString(theSeverity);
211                severityChild.getMutator().addValue(theIssue, severityElem);
212
213                IPrimitiveType<?> string = (IPrimitiveType<?>) stringDef.newInstance();
214                string.setValueAsString(theDetails);
215                diagnosticsChild.getMutator().setValue(theIssue, string);
216
217                addLocationToIssue(theCtx, theIssue, theLocation);
218
219                if (isNotBlank(theDetailSystem)) {
220                        BaseRuntimeChildDefinition detailsChild = issueElement.getChildByName("details");
221                        if (detailsChild != null) {
222                                BaseRuntimeElementDefinition<?> codeableConceptDef = theCtx.getElementDefinition("CodeableConcept");
223                                IBase codeableConcept = codeableConceptDef.newInstance();
224
225                                BaseRuntimeElementDefinition<?> codingDef = theCtx.getElementDefinition("Coding");
226                                IBaseCoding coding = (IBaseCoding) codingDef.newInstance();
227                                coding.setSystem(theDetailSystem);
228                                coding.setCode(theDetailCode);
229                                coding.setDisplay(theDetailDescription);
230
231                                codeableConceptDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding);
232
233                                detailsChild.getMutator().addValue(theIssue, codeableConcept);
234                        }
235                }
236        }
237
238        public static void addLocationToIssue(FhirContext theContext, IBase theIssue, String theLocation) {
239                if (isNotBlank(theLocation)) {
240                        BaseRuntimeElementCompositeDefinition<?> issueElement =
241                                        (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theIssue.getClass());
242                        BaseRuntimeChildDefinition locationChild = issueElement.getChildByName("location");
243                        IPrimitiveType<?> locationElem = (IPrimitiveType<?>) locationChild
244                                        .getChildByName("location")
245                                        .newInstance(locationChild.getInstanceConstructorArguments());
246                        locationElem.setValueAsString(theLocation);
247                        locationChild.getMutator().addValue(theIssue, locationElem);
248                }
249        }
250
251        public static IBase addIssueWithMessageId(
252                        FhirContext myCtx,
253                        IBaseOperationOutcome theOperationOutcome,
254                        String theSeverity,
255                        String theMessage,
256                        String theMessageId,
257                        String theLocation,
258                        String theCode) {
259                IBase issue = addIssue(myCtx, theOperationOutcome, theSeverity, theMessage, theLocation, theCode);
260                if (isNotBlank(theMessageId)) {
261                        addDetailsToIssue(myCtx, issue, Constants.JAVA_VALIDATOR_DETAILS_SYSTEM, theMessageId);
262                }
263
264                return issue;
265        }
266
267        public static void addDetailsToIssue(FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode) {
268                BaseRuntimeElementCompositeDefinition<?> issueElement =
269                                (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(theIssue.getClass());
270                BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details");
271
272                BaseRuntimeElementCompositeDefinition<?> codingDef =
273                                (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("Coding");
274                ICompositeType coding = (ICompositeType) codingDef.newInstance();
275
276                // System
277                IPrimitiveType<?> system =
278                                (IPrimitiveType<?>) theFhirContext.getElementDefinition("uri").newInstance();
279                system.setValueAsString(theSystem);
280                codingDef.getChildByName("system").getMutator().addValue(coding, system);
281
282                // Code
283                IPrimitiveType<?> code =
284                                (IPrimitiveType<?>) theFhirContext.getElementDefinition("code").newInstance();
285                code.setValueAsString(theCode);
286                codingDef.getChildByName("code").getMutator().addValue(coding, code);
287                BaseRuntimeElementCompositeDefinition<?> ccDef =
288                                (BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition("CodeableConcept");
289
290                ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance();
291                ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding);
292                detailsChildDef.getMutator().addValue(theIssue, codeableConcept);
293        }
294
295        public static void addIssueLineExtensionToIssue(FhirContext theCtx, IBase theIssue, String theLine) {
296                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
297                        ExtensionUtil.setExtension(
298                                        theCtx,
299                                        theIssue,
300                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line",
301                                        "integer",
302                                        theLine);
303                }
304        }
305
306        public static void addIssueColExtensionToIssue(FhirContext theCtx, IBase theIssue, String theColumn) {
307                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
308                        ExtensionUtil.setExtension(
309                                        theCtx,
310                                        theIssue,
311                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col",
312                                        "integer",
313                                        theColumn);
314                }
315        }
316
317        public static void addMessageIdExtensionToIssue(FhirContext theCtx, IBase theIssue, String theMessageId) {
318                if (theCtx.getVersion().getVersion() != FhirVersionEnum.DSTU2) {
319                        ExtensionUtil.setExtension(
320                                        theCtx,
321                                        theIssue,
322                                        "http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id",
323                                        "string",
324                                        theMessageId);
325                }
326        }
327}