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