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}