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