
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}