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.validation;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.rest.api.Constants;
024import ca.uhn.fhir.util.OperationOutcomeUtil;
025import org.hl7.fhir.instance.model.api.IBase;
026import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
027
028import java.util.Collections;
029import java.util.List;
030
031import static org.apache.commons.lang3.StringUtils.isNotBlank;
032
033/**
034 * Encapsulates the results of validation
035 *
036 * @see ca.uhn.fhir.validation.FhirValidator
037 * @since 0.7
038 */
039public class ValidationResult {
040        public static final int ERROR_DISPLAY_LIMIT_DEFAULT = 1;
041        public static final String UNKNOWN = "(unknown)";
042        private static final String ourNewLine = System.getProperty("line.separator");
043        private final FhirContext myCtx;
044        private final boolean myIsSuccessful;
045        private final List<SingleValidationMessage> myMessages;
046        private int myErrorDisplayLimit = ERROR_DISPLAY_LIMIT_DEFAULT;
047
048        public ValidationResult(FhirContext theCtx, List<SingleValidationMessage> theMessages) {
049                boolean successful = true;
050                myCtx = theCtx;
051                myMessages = theMessages;
052                for (SingleValidationMessage next : myMessages) {
053                        if (next.getSeverity() == null || next.getSeverity().ordinal() > ResultSeverityEnum.WARNING.ordinal()) {
054                                successful = false;
055                                break;
056                        }
057                }
058                myIsSuccessful = successful;
059        }
060
061        public List<SingleValidationMessage> getMessages() {
062                return Collections.unmodifiableList(myMessages);
063        }
064
065        /**
066         * Was the validation successful (in other words, do we have no issues that are at
067         * severity {@link ResultSeverityEnum#ERROR} or {@link ResultSeverityEnum#FATAL}. A validation
068         * is still considered successful if it only has issues at level {@link ResultSeverityEnum#WARNING} or
069         * lower.
070         *
071         * @return true if the validation was successful
072         */
073        public boolean isSuccessful() {
074                return myIsSuccessful;
075        }
076
077        private String toDescription() {
078                if (myMessages.isEmpty()) {
079                        return "No issues";
080                }
081
082                StringBuilder b = new StringBuilder(100 * myMessages.size());
083                int shownMsgQty = Math.min(myErrorDisplayLimit, myMessages.size());
084
085                if (shownMsgQty < myMessages.size()) {
086                        b.append("(showing first ")
087                                        .append(shownMsgQty)
088                                        .append(" messages out of ")
089                                        .append(myMessages.size())
090                                        .append(" total)")
091                                        .append(ourNewLine);
092                }
093
094                for (int i = 0; i < shownMsgQty; i++) {
095                        SingleValidationMessage nextMsg = myMessages.get(i);
096                        b.append(ourNewLine);
097                        if (nextMsg.getSeverity() != null) {
098                                b.append(nextMsg.getSeverity().name());
099                                b.append(" - ");
100                        }
101                        b.append(nextMsg.getMessage());
102                        b.append(" - ");
103                        b.append(nextMsg.getLocationString());
104                }
105
106                return b.toString();
107        }
108
109        /**
110         * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view.
111         *    {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method
112         *      will be removed at some point.
113         */
114        @Deprecated
115        public IBaseOperationOutcome getOperationOutcome() {
116                return toOperationOutcome();
117        }
118
119        /**
120         * Create an OperationOutcome resource which contains all the messages found as a result of this validation
121         */
122        public IBaseOperationOutcome toOperationOutcome() {
123                IBaseOperationOutcome oo = (IBaseOperationOutcome)
124                                myCtx.getResourceDefinition("OperationOutcome").newInstance();
125                populateOperationOutcome(oo);
126                return oo;
127        }
128
129        /**
130         * Populate an operation outcome with the results of the validation
131         */
132        public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
133                for (SingleValidationMessage next : myMessages) {
134                        Integer locationLine = next.getLocationLine();
135                        Integer locationCol = next.getLocationCol();
136                        String location = next.getLocationString();
137                        ResultSeverityEnum issueSeverity = next.getSeverity();
138                        String message = next.getMessage();
139                        String messageId = next.getMessageId();
140
141                        if (next.getSliceMessages() == null) {
142                                addIssueToOperationOutcome(
143                                                theOperationOutcome, location, locationLine, locationCol, issueSeverity, message, messageId);
144                                continue;
145                        }
146
147                        /*
148                         * Occasionally the validator will return these lists of "slice messages"
149                         * which happen when validating rules associated with a specific slice in
150                         * a profile.
151                         */
152                        for (String nextSliceMessage : next.getSliceMessages()) {
153                                String combinedMessage = message + " - " + nextSliceMessage;
154                                addIssueToOperationOutcome(
155                                                theOperationOutcome,
156                                                location,
157                                                locationLine,
158                                                locationCol,
159                                                issueSeverity,
160                                                combinedMessage,
161                                                messageId);
162                        }
163                } // for
164
165                if (myMessages.isEmpty()) {
166                        String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected");
167                        OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, "information", message, null, "informational");
168                }
169        }
170
171        /**
172         * Adds a repetition of <code>OperationOutcome.issue</code> to an
173         * <code>OperationOutcome</code> instance.
174         *
175         * @param theOperationOutcome   The OperationOutcome to add to
176         * @param theLocationExpression The FHIRPath expression describing where the issue was found
177         * @param theLocationLine       The line number in the source where the issue was found
178         * @param theLocationCol        The column number in the source where the issue was found
179         * @param theIssueSeverity      The severity code (must be a valid value for <code>OperationOutcome.issue.severity</code>
180         * @param theMessage            The validation message
181         * @param theMessageId          The java validator message ID
182         */
183        private void addIssueToOperationOutcome(
184                        IBaseOperationOutcome theOperationOutcome,
185                        String theLocationExpression,
186                        Integer theLocationLine,
187                        Integer theLocationCol,
188                        ResultSeverityEnum theIssueSeverity,
189                        String theMessage,
190                        String theMessageId) {
191
192                String severity = theIssueSeverity != null ? theIssueSeverity.getCode() : null;
193                IBase issue = OperationOutcomeUtil.addIssueWithMessageId(
194                                myCtx,
195                                theOperationOutcome,
196                                severity,
197                                theMessage,
198                                theMessageId,
199                                theLocationExpression,
200                                Constants.OO_INFOSTATUS_PROCESSING);
201
202                if (theLocationLine != null || theLocationCol != null) {
203                        String unknown = UNKNOWN;
204                        String line = unknown;
205                        if (theLocationLine != null && theLocationLine != -1) {
206                                line = theLocationLine.toString();
207                        }
208                        String col = unknown;
209                        if (theLocationCol != null && theLocationCol != -1) {
210                                col = theLocationCol.toString();
211                        }
212                        if (!unknown.equals(line) || !unknown.equals(col)) {
213                                OperationOutcomeUtil.addIssueLineExtensionToIssue(myCtx, issue, line);
214                                OperationOutcomeUtil.addIssueColExtensionToIssue(myCtx, issue, col);
215                                String locationString = "Line[" + line + "] Col[" + col + "]";
216                                OperationOutcomeUtil.addLocationToIssue(myCtx, issue, locationString);
217                        }
218                }
219
220                if (isNotBlank(theLocationExpression)) {
221                        OperationOutcomeUtil.addExpressionToIssue(myCtx, issue, theLocationExpression);
222                }
223
224                if (isNotBlank(theMessageId)) {
225                        OperationOutcomeUtil.addMessageIdExtensionToIssue(myCtx, issue, theMessageId);
226                }
227        }
228
229        @Override
230        public String toString() {
231                return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful
232                                + ", description='" + toDescription() + '\'' + '}';
233        }
234
235        /**
236         * @since 5.5.0
237         */
238        public FhirContext getContext() {
239                return myCtx;
240        }
241
242        public int getErrorDisplayLimit() {
243                return myErrorDisplayLimit;
244        }
245
246        public void setErrorDisplayLimit(int theErrorDisplayLimit) {
247                myErrorDisplayLimit = theErrorDisplayLimit;
248        }
249}