001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.rest.api.Constants;
025import ca.uhn.fhir.util.OperationOutcomeUtil;
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
043        private final FhirContext myCtx;
044        private final boolean myIsSuccessful;
045        private final List<SingleValidationMessage> myMessages;
046
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
079        private String toDescription() {
080                if (myMessages.isEmpty()) {
081                        return "No issues";
082                }
083
084                StringBuilder b = new StringBuilder(100 * myMessages.size());
085                int shownMsgQty = Math.min(myErrorDisplayLimit, myMessages.size());
086
087                if (shownMsgQty < myMessages.size()) {
088                        b.append("(showing first ").append(shownMsgQty).append(" messages out of ")
089                                .append(myMessages.size()).append(" total)").append(ourNewLine);
090                }
091
092                for (int i = 0; i < shownMsgQty; i++) {
093                        SingleValidationMessage nextMsg = myMessages.get(i);
094                        b.append(ourNewLine);
095                        if (nextMsg.getSeverity() != null) {
096                                b.append(nextMsg.getSeverity().name());
097                                b.append(" - ");
098                        }
099                        b.append(nextMsg.getMessage());
100                        b.append(" - ");
101                        b.append(nextMsg.getLocationString());
102                }
103
104                return b.toString();
105        }
106
107
108        /**
109         * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view.
110         *             {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method
111         *             will be removed at some point.
112         */
113        @Deprecated
114        public IBaseOperationOutcome getOperationOutcome() {
115                return toOperationOutcome();
116        }
117
118        /**
119         * Create an OperationOutcome resource which contains all of the messages found as a result of this validation
120         */
121        public IBaseOperationOutcome toOperationOutcome() {
122                IBaseOperationOutcome oo = (IBaseOperationOutcome) myCtx.getResourceDefinition("OperationOutcome").newInstance();
123                populateOperationOutcome(oo);
124                return oo;
125        }
126
127        /**
128         * Populate an operation outcome with the results of the validation 
129         */
130        public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) {
131                for (SingleValidationMessage next : myMessages) {
132                        String location;
133                        if (isNotBlank(next.getLocationString())) {
134                                location = next.getLocationString();
135                        } else if (next.getLocationLine() != null || next.getLocationCol() != null) {
136                                location = "Line[" + next.getLocationLine() + "] Col[" + next.getLocationCol() + "]";
137                        } else {
138                                location = null;
139                        }
140                        String severity = next.getSeverity() != null ? next.getSeverity().getCode() : null;
141                        IBase issue = OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, severity, next.getMessage(), location, Constants.OO_INFOSTATUS_PROCESSING);
142                        
143                        if (next.getLocationLine() != null || next.getLocationCol() != null) {
144                                String unknown = "(unknown)";
145                                String line = unknown;
146                                if (next.getLocationLine() != null && next.getLocationLine() != -1) {
147                                        line = next.getLocationLine().toString();
148                                }
149                                String col = unknown;
150                                if (next.getLocationCol() != null && next.getLocationCol() != -1) {
151                                        col = next.getLocationCol().toString();
152                                }
153                                if (!unknown.equals(line) || !unknown.equals(col)) {
154                                        OperationOutcomeUtil.addLocationToIssue(myCtx, issue, "Line " + line + ", Col " + col);
155                                }
156                        }
157                }
158
159                if (myMessages.isEmpty()) {
160                        String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected");
161                        OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, "information", message, null, "informational");
162                }
163        }
164
165        @Override
166        public String toString() {
167                return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful + ", description='" + toDescription() + '\'' + '}';
168        }
169
170        /**
171         * @since 5.5.0
172         */
173        public FhirContext getContext() {
174                return myCtx;
175        }
176
177        public int getErrorDisplayLimit() { return myErrorDisplayLimit; }
178
179        public void setErrorDisplayLimit(int theErrorDisplayLimit) { myErrorDisplayLimit = theErrorDisplayLimit; }
180
181
182        private static final String  ourNewLine = System.getProperty("line.separator");
183}