001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.cli.util; 007 008import static org.fusesource.jansi.Ansi.ansi; 009 010import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding; 011import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; 012import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor; 013import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding; 014import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding; 015import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding; 016import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler; 017 018import org.apache.logging.log4j.LogBuilder; 019import org.apache.logging.log4j.LogManager; 020import org.apache.logging.log4j.Logger; 021import org.fusesource.jansi.Ansi; 022import org.fusesource.jansi.Ansi.Color; 023import org.xml.sax.SAXParseException; 024 025import java.net.URI; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import edu.umd.cs.findbugs.annotations.NonNull; 030import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 031 032/** 033 * Supports logging validation findings to the console using ANSI color codes to 034 * improve the visibility of warnings and errors. 035 */ 036public final class LoggingValidationHandler 037 extends AbstractValidationResultProcessor { 038 private static final Logger LOGGER = LogManager.getLogger(LoggingValidationHandler.class); 039 040 @NonNull 041 private static final LoggingValidationHandler NO_LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(false); 042 @NonNull 043 private static final LoggingValidationHandler LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(true); 044 045 private final boolean logExceptions; 046 047 /** 048 * Get a singleton instance of the logging validation handler. 049 * <p> 050 * This instance will not log exceptions. 051 * 052 * @return the instance 053 */ 054 @NonNull 055 public static LoggingValidationHandler instance() { 056 return instance(false); 057 } 058 059 /** 060 * Get a singleton instance of the logging validation handler. 061 * 062 * @param logExceptions 063 * {@code true} if this instance will log exceptions or {@code false} 064 * otherwise 065 * @return the instance 066 */ 067 @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", 068 justification = "both values are class initialized") 069 @NonNull 070 public static LoggingValidationHandler instance(boolean logExceptions) { 071 return logExceptions ? LOG_EXCPTION_INSTANCE : NO_LOG_EXCPTION_INSTANCE; 072 } 073 074 private LoggingValidationHandler(boolean logExceptions) { 075 this.logExceptions = logExceptions; 076 } 077 078 /** 079 * Determine if exceptions should be logged. 080 * 081 * @return {@code true} if exceptions are logged or {@code false} otherwise 082 */ 083 public boolean isLogExceptions() { 084 return logExceptions; 085 } 086 087 @Override 088 protected void handleJsonValidationFinding(@NonNull JsonValidationFinding finding) { 089 Ansi ansi = generatePreamble(finding.getSeverity()); 090 091 ansi = ansi.a('[') 092 .fgBright(Color.WHITE) 093 .a(finding.getCause().getPointerToViolation()) 094 .reset() 095 .a(']'); 096 097 URI documentUri = finding.getDocumentUri(); 098 ansi = documentUri == null 099 ? ansi.format(" %s", finding.getMessage()) 100 : ansi.format(" %s [%s]", finding.getMessage(), documentUri.toString()); 101 102 getLogger(finding).log(ansi); 103 } 104 105 @Override 106 protected void handleXmlValidationFinding(XmlValidationFinding finding) { 107 Ansi ansi = generatePreamble(finding.getSeverity()); 108 SAXParseException ex = finding.getCause(); 109 110 URI documentUri = finding.getDocumentUri(); 111 ansi = documentUri == null 112 ? ansi.format("%s [{%d,%d}]", 113 finding.getMessage(), 114 ex.getLineNumber(), 115 ex.getColumnNumber()) 116 : ansi.format("%s [%s{%d,%d}]", 117 finding.getMessage(), 118 documentUri.toString(), 119 ex.getLineNumber(), 120 ex.getColumnNumber()); 121 122 getLogger(finding).log(ansi); 123 } 124 125 @Override 126 protected void handleConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) { 127 Ansi ansi = generatePreamble(finding.getSeverity()); 128 129 ansi.format("[%s]", finding.getTarget().getMetapath()); 130 131 String id = finding.getIdentifier(); 132 if (id != null) { 133 ansi.format(" %s:", id); 134 } 135 136 ansi.format(" %s", finding.getMessage()); 137 138 Set<String> helpUrls = finding.getConstraints().stream() 139 .flatMap(constraint -> constraint.getPropertyValues(SarifValidationHandler.SARIF_HELP_URL_KEY).stream()) 140 .collect(Collectors.toSet()); 141 if (!helpUrls.isEmpty()) { 142 ansi.format(" (help: %s)", 143 helpUrls.stream().collect(Collectors.joining(", "))); 144 } 145 146 getLogger(finding).log(ansi); 147 } 148 149 @NonNull 150 private LogBuilder getLogger(@NonNull IValidationFinding finding) { 151 LogBuilder retval; 152 switch (finding.getSeverity()) { 153 case CRITICAL: 154 retval = LOGGER.atFatal(); 155 break; 156 case ERROR: 157 retval = LOGGER.atError(); 158 break; 159 case WARNING: 160 retval = LOGGER.atWarn(); 161 break; 162 case INFORMATIONAL: 163 retval = LOGGER.atInfo(); 164 break; 165 case DEBUG: 166 retval = LOGGER.isDebugEnabled() ? LOGGER.atDebug() : LOGGER.atInfo(); 167 break; 168 default: 169 throw new IllegalArgumentException("Unknown level: " + finding.getSeverity().name()); 170 } 171 172 assert retval != null; 173 174 if (finding.getCause() != null && isLogExceptions()) { 175 retval.withThrowable(finding.getCause()); 176 } 177 178 return retval; 179 } 180 181 @SuppressWarnings("static-method") 182 @NonNull 183 private Ansi generatePreamble(@NonNull Level level) { 184 Ansi ansi = ansi().fgBright(Color.WHITE).a('[').reset(); 185 186 switch (level) { 187 case CRITICAL: 188 ansi = ansi.fgRed().a("CRITICAL").reset(); 189 break; 190 case ERROR: 191 ansi = ansi.fgBrightRed().a("ERROR").reset(); 192 break; 193 case WARNING: 194 ansi = ansi.fgBrightYellow().a("WARNING").reset(); 195 break; 196 case INFORMATIONAL: 197 ansi = ansi.fgBrightBlue().a("INFO").reset(); 198 break; 199 case DEBUG: 200 ansi = ansi.fgBrightCyan().a("DEBUG").reset(); 201 break; 202 default: 203 ansi = ansi().fgBright(Color.MAGENTA).a(level.name()).reset(); 204 break; 205 } 206 ansi = ansi.fgBright(Color.WHITE).a("] ").reset(); 207 208 assert ansi != null; 209 return ansi; 210 } 211}