001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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
042        private final FhirContext myCtx;
043        private final boolean myIsSuccessful;
044        private final List<SingleValidationMessage> myMessages;
045
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
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 ").append(shownMsgQty).append(" messages out of ")
088                                .append(myMessages.size()).append(" total)").append(ourNewLine);
089                }
090
091                for (int i = 0; i < shownMsgQty; i++) {
092                        SingleValidationMessage nextMsg = myMessages.get(i);
093                        b.append(ourNewLine);
094                        if (nextMsg.getSeverity() != null) {
095                                b.append(nextMsg.getSeverity().name());
096                                b.append(" - ");
097                        }
098                        b.append(nextMsg.getMessage());
099                        b.append(" - ");
100                        b.append(nextMsg.getLocationString());
101                }
102
103                return b.toString();
104        }
105
106
107        /**
108         * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view.
109         *             {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method
110         *             will be removed at some point.
111         */
112        @Deprecated
113        public IBaseOperationOutcome getOperationOutcome() {
114                return toOperationOutcome();
115        }
116
117        /**
118         * Create an OperationOutcome resource which contains all of the messages found as a result of this validation
119         */
120        public IBaseOperationOutcome toOperationOutcome() {
121                IBaseOperationOutcome oo = (IBaseOperationOutcome) myCtx.getResourceDefinition("OperationOutcome").newInstance();
122                populateOperationOutcome(oo);
123                return oo;
124        }
125
126        /**
127         * Populate an operation outcome with the results of the validation 
128         */
129        public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
130                for (SingleValidationMessage next : myMessages) {
131                        String location;
132                        if (isNotBlank(next.getLocationString())) {
133                                location = next.getLocationString();
134                        } else if (next.getLocationLine() != null || next.getLocationCol() != null) {
135                                location = "Line[" + next.getLocationLine() + "] Col[" + next.getLocationCol() + "]";
136                        } else {
137                                location = null;
138                        }
139                        String severity = next.getSeverity() != null ? next.getSeverity().getCode() : null;
140                        IBase issue = OperationOutcomeUtil.addIssueWithMessageId(myCtx, theOperationOutcome, severity, next.getMessage(), next.getMessageId(), location, Constants.OO_INFOSTATUS_PROCESSING);
141                        
142                        if (next.getLocationLine() != null || next.getLocationCol() != null) {
143                                String unknown = "(unknown)";
144                                String line = unknown;
145                                if (next.getLocationLine() != null && next.getLocationLine() != -1) {
146                                        line = next.getLocationLine().toString();
147                                }
148                                String col = unknown;
149                                if (next.getLocationCol() != null && next.getLocationCol() != -1) {
150                                        col = next.getLocationCol().toString();
151                                }
152                                if (!unknown.equals(line) || !unknown.equals(col)) {
153                                        OperationOutcomeUtil.addLocationToIssue(myCtx, issue, "Line " + line + ", Col " + col);
154                                }
155                        }
156                }
157
158                if (myMessages.isEmpty()) {
159                        String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected");
160                        OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, "information", message, null, "informational");
161                }
162        }
163
164        @Override
165        public String toString() {
166                return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful + ", description='" + toDescription() + '\'' + '}';
167        }
168
169        /**
170         * @since 5.5.0
171         */
172        public FhirContext getContext() {
173                return myCtx;
174        }
175
176        public int getErrorDisplayLimit() { return myErrorDisplayLimit; }
177
178        public void setErrorDisplayLimit(int theErrorDisplayLimit) { myErrorDisplayLimit = theErrorDisplayLimit; }
179
180
181        private static final String  ourNewLine = System.getProperty("line.separator");
182}