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.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.isBlank;
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 final 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        /**
067         * Was the validation successful (in other words, do we have no issues that are at
068         * severity {@link ResultSeverityEnum#ERROR} or {@link ResultSeverityEnum#FATAL}. A validation
069         * is still considered successful if it only has issues at level {@link ResultSeverityEnum#WARNING} or
070         * lower.
071         *
072         * @return true if the validation was successful
073         */
074        public boolean isSuccessful() {
075                return myIsSuccessful;
076        }
077
078        private String toDescription() {
079                if (myMessages.isEmpty()) {
080                        return "No issues";
081                }
082
083                StringBuilder b = new StringBuilder(100 * myMessages.size());
084                int shownMsgQty = Math.min(myErrorDisplayLimit, myMessages.size());
085
086                if (shownMsgQty < myMessages.size()) {
087                        b.append("(showing first ")
088                                        .append(shownMsgQty)
089                                        .append(" messages out of ")
090                                        .append(myMessages.size())
091                                        .append(" total)")
092                                        .append(ourNewLine);
093                }
094
095                for (int i = 0; i < shownMsgQty; i++) {
096                        SingleValidationMessage nextMsg = myMessages.get(i);
097                        b.append(ourNewLine);
098                        if (nextMsg.getSeverity() != null) {
099                                b.append(nextMsg.getSeverity().name());
100                                b.append(" - ");
101                        }
102                        b.append(nextMsg.getMessage());
103                        b.append(" - ");
104                        b.append(nextMsg.getLocationString());
105                }
106
107                return b.toString();
108        }
109
110        /**
111         * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view.
112         * {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method
113         * will be removed at some point.
114         */
115        @Deprecated
116        public IBaseOperationOutcome getOperationOutcome() {
117                return toOperationOutcome();
118        }
119
120        /**
121         * Create an OperationOutcome resource which contains all of the messages found as a result of this validation
122         */
123        public IBaseOperationOutcome toOperationOutcome() {
124                IBaseOperationOutcome oo = (IBaseOperationOutcome)
125                                myCtx.getResourceDefinition("OperationOutcome").newInstance();
126                populateOperationOutcome(oo);
127                return oo;
128        }
129
130        /**
131         * Populate an operation outcome with the results of the validation
132         */
133        public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
134                for (SingleValidationMessage next : myMessages) {
135                        Integer locationLine = next.getLocationLine();
136                        Integer locationCol = next.getLocationCol();
137                        String location = next.getLocationString();
138                        ResultSeverityEnum issueSeverity = next.getSeverity();
139                        String message = next.getMessage();
140                        String messageId = next.getMessageId();
141
142                        if (next.getSliceMessages() == null) {
143                                addIssueToOperationOutcome(
144                                                theOperationOutcome, location, locationLine, locationCol, issueSeverity, message, messageId);
145                                continue;
146                        }
147
148                        /*
149                         * Occasionally the validator will return these lists of "slice messages"
150                         * which happen when validating rules associated with a specific slice in
151                         * a profile.
152                         */
153                        for (String nextSliceMessage : next.getSliceMessages()) {
154                                String combinedMessage = message + " - " + nextSliceMessage;
155                                addIssueToOperationOutcome(
156                                                theOperationOutcome,
157                                                location,
158                                                locationLine,
159                                                locationCol,
160                                                issueSeverity,
161                                                combinedMessage,
162                                                messageId);
163                        }
164                } // for
165
166                if (myMessages.isEmpty()) {
167                        String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected");
168                        OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, "information", message, null, "informational");
169                }
170        }
171
172        private void addIssueToOperationOutcome(
173                        IBaseOperationOutcome theOperationOutcome,
174                        String location,
175                        Integer locationLine,
176                        Integer locationCol,
177                        ResultSeverityEnum issueSeverity,
178                        String message,
179                        String messageId) {
180                if (isBlank(location) && locationLine != null && locationCol != null) {
181                        location = "Line[" + locationLine + "] Col[" + locationCol + "]";
182                }
183                String severity = issueSeverity != null ? issueSeverity.getCode() : null;
184                IBase issue = OperationOutcomeUtil.addIssueWithMessageId(
185                                myCtx, theOperationOutcome, severity, message, messageId, location, Constants.OO_INFOSTATUS_PROCESSING);
186
187                if (locationLine != null || locationCol != null) {
188                        String unknown = UNKNOWN;
189                        String line = unknown;
190                        if (locationLine != null && locationLine != -1) {
191                                line = locationLine.toString();
192                        }
193                        String col = unknown;
194                        if (locationCol != null && locationCol != -1) {
195                                col = locationCol.toString();
196                        }
197                        if (!unknown.equals(line) || !unknown.equals(col)) {
198                                OperationOutcomeUtil.addIssueLineExtensionToIssue(myCtx, issue, line);
199                                OperationOutcomeUtil.addIssueColExtensionToIssue(myCtx, issue, col);
200                                String locationString = "Line[" + line + "] Col[" + col + "]";
201                                OperationOutcomeUtil.addLocationToIssue(myCtx, issue, locationString);
202                        }
203                }
204
205                if (isNotBlank(messageId)) {
206                        OperationOutcomeUtil.addMessageIdExtensionToIssue(myCtx, issue, messageId);
207                }
208        }
209
210        @Override
211        public String toString() {
212                return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful
213                                + ", description='" + toDescription() + '\'' + '}';
214        }
215
216        /**
217         * @since 5.5.0
218         */
219        public FhirContext getContext() {
220                return myCtx;
221        }
222
223        public int getErrorDisplayLimit() {
224                return myErrorDisplayLimit;
225        }
226
227        public void setErrorDisplayLimit(int theErrorDisplayLimit) {
228                myErrorDisplayLimit = theErrorDisplayLimit;
229        }
230}