
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.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 public static final String UNKNOWN = "(unknown)"; 042 private static final String ourNewLine = System.getProperty("line.separator"); 043 private final FhirContext myCtx; 044 private final boolean myIsSuccessful; 045 private final List<SingleValidationMessage> myMessages; 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 private String toDescription() { 078 if (myMessages.isEmpty()) { 079 return "No issues"; 080 } 081 082 StringBuilder b = new StringBuilder(100 * myMessages.size()); 083 int shownMsgQty = Math.min(myErrorDisplayLimit, myMessages.size()); 084 085 if (shownMsgQty < myMessages.size()) { 086 b.append("(showing first ") 087 .append(shownMsgQty) 088 .append(" messages out of ") 089 .append(myMessages.size()) 090 .append(" total)") 091 .append(ourNewLine); 092 } 093 094 for (int i = 0; i < shownMsgQty; i++) { 095 SingleValidationMessage nextMsg = myMessages.get(i); 096 b.append(ourNewLine); 097 if (nextMsg.getSeverity() != null) { 098 b.append(nextMsg.getSeverity().name()); 099 b.append(" - "); 100 } 101 b.append(nextMsg.getMessage()); 102 b.append(" - "); 103 b.append(nextMsg.getLocationString()); 104 } 105 106 return b.toString(); 107 } 108 109 /** 110 * @deprecated Use {@link #toOperationOutcome()} instead since this method returns a view. 111 * {@link #toOperationOutcome()} is identical to this method, but has a more suitable name so this method 112 * will be removed at some point. 113 */ 114 @Deprecated 115 public IBaseOperationOutcome getOperationOutcome() { 116 return toOperationOutcome(); 117 } 118 119 /** 120 * Create an OperationOutcome resource which contains all the messages found as a result of this validation 121 */ 122 public IBaseOperationOutcome toOperationOutcome() { 123 IBaseOperationOutcome oo = (IBaseOperationOutcome) 124 myCtx.getResourceDefinition("OperationOutcome").newInstance(); 125 populateOperationOutcome(oo); 126 return oo; 127 } 128 129 /** 130 * Populate an operation outcome with the results of the validation 131 */ 132 public void populateOperationOutcome(IBaseOperationOutcome theOperationOutcome) { 133 for (SingleValidationMessage next : myMessages) { 134 Integer locationLine = next.getLocationLine(); 135 Integer locationCol = next.getLocationCol(); 136 String location = next.getLocationString(); 137 ResultSeverityEnum issueSeverity = next.getSeverity(); 138 String message = next.getMessage(); 139 String messageId = next.getMessageId(); 140 141 if (next.getSliceMessages() == null) { 142 addIssueToOperationOutcome( 143 theOperationOutcome, location, locationLine, locationCol, issueSeverity, message, messageId); 144 continue; 145 } 146 147 /* 148 * Occasionally the validator will return these lists of "slice messages" 149 * which happen when validating rules associated with a specific slice in 150 * a profile. 151 */ 152 for (String nextSliceMessage : next.getSliceMessages()) { 153 String combinedMessage = message + " - " + nextSliceMessage; 154 addIssueToOperationOutcome( 155 theOperationOutcome, 156 location, 157 locationLine, 158 locationCol, 159 issueSeverity, 160 combinedMessage, 161 messageId); 162 } 163 } // for 164 165 if (myMessages.isEmpty()) { 166 String message = myCtx.getLocalizer().getMessage(ValidationResult.class, "noIssuesDetected"); 167 OperationOutcomeUtil.addIssue(myCtx, theOperationOutcome, "information", message, null, "informational"); 168 } 169 } 170 171 /** 172 * Adds a repetition of <code>OperationOutcome.issue</code> to an 173 * <code>OperationOutcome</code> instance. 174 * 175 * @param theOperationOutcome The OperationOutcome to add to 176 * @param theLocationExpression The FHIRPath expression describing where the issue was found 177 * @param theLocationLine The line number in the source where the issue was found 178 * @param theLocationCol The column number in the source where the issue was found 179 * @param theIssueSeverity The severity code (must be a valid value for <code>OperationOutcome.issue.severity</code> 180 * @param theMessage The validation message 181 * @param theMessageId The java validator message ID 182 */ 183 private void addIssueToOperationOutcome( 184 IBaseOperationOutcome theOperationOutcome, 185 String theLocationExpression, 186 Integer theLocationLine, 187 Integer theLocationCol, 188 ResultSeverityEnum theIssueSeverity, 189 String theMessage, 190 String theMessageId) { 191 192 String severity = theIssueSeverity != null ? theIssueSeverity.getCode() : null; 193 IBase issue = OperationOutcomeUtil.addIssueWithMessageId( 194 myCtx, 195 theOperationOutcome, 196 severity, 197 theMessage, 198 theMessageId, 199 theLocationExpression, 200 Constants.OO_INFOSTATUS_PROCESSING); 201 202 if (theLocationLine != null || theLocationCol != null) { 203 String unknown = UNKNOWN; 204 String line = unknown; 205 if (theLocationLine != null && theLocationLine != -1) { 206 line = theLocationLine.toString(); 207 } 208 String col = unknown; 209 if (theLocationCol != null && theLocationCol != -1) { 210 col = theLocationCol.toString(); 211 } 212 if (!unknown.equals(line) || !unknown.equals(col)) { 213 OperationOutcomeUtil.addIssueLineExtensionToIssue(myCtx, issue, line); 214 OperationOutcomeUtil.addIssueColExtensionToIssue(myCtx, issue, col); 215 String locationString = "Line[" + line + "] Col[" + col + "]"; 216 OperationOutcomeUtil.addLocationToIssue(myCtx, issue, locationString); 217 } 218 } 219 220 if (isNotBlank(theLocationExpression)) { 221 OperationOutcomeUtil.addExpressionToIssue(myCtx, issue, theLocationExpression); 222 } 223 224 if (isNotBlank(theMessageId)) { 225 OperationOutcomeUtil.addMessageIdExtensionToIssue(myCtx, issue, theMessageId); 226 } 227 } 228 229 @Override 230 public String toString() { 231 return "ValidationResult{" + "messageCount=" + myMessages.size() + ", isSuccessful=" + myIsSuccessful 232 + ", description='" + toDescription() + '\'' + '}'; 233 } 234 235 /** 236 * @since 5.5.0 237 */ 238 public FhirContext getContext() { 239 return myCtx; 240 } 241 242 public int getErrorDisplayLimit() { 243 return myErrorDisplayLimit; 244 } 245 246 public void setErrorDisplayLimit(int theErrorDisplayLimit) { 247 myErrorDisplayLimit = theErrorDisplayLimit; 248 } 249}