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}