LoggingValidationHandler.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.cli.util;

import static org.fusesource.jansi.Ansi.ansi;

import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor;
import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;

import org.apache.logging.log4j.LogBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Color;
import org.xml.sax.SAXParseException;

import java.net.URI;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Supports logging validation findings to the console using ANSI color codes to
 * improve the visibility of warnings and errors.
 */
public final class LoggingValidationHandler
    extends AbstractValidationResultProcessor {
  private static final Logger LOGGER = LogManager.getLogger(LoggingValidationHandler.class);

  @NonNull
  private static final LoggingValidationHandler NO_LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(false);
  @NonNull
  private static final LoggingValidationHandler LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(true);

  private final boolean logExceptions;

  /**
   * Get a singleton instance of the logging validation handler.
   * <p>
   * This instance will not log exceptions.
   *
   * @return the instance
   */
  @NonNull
  public static LoggingValidationHandler instance() {
    return instance(false);
  }

  /**
   * Get a singleton instance of the logging validation handler.
   *
   * @param logExceptions
   *          {@code true} if this instance will log exceptions or {@code false}
   *          otherwise
   * @return the instance
   */
  @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED",
      justification = "both values are class initialized")
  @NonNull
  public static LoggingValidationHandler instance(boolean logExceptions) {
    return logExceptions ? LOG_EXCPTION_INSTANCE : NO_LOG_EXCPTION_INSTANCE;
  }

  private LoggingValidationHandler(boolean logExceptions) {
    this.logExceptions = logExceptions;
  }

  /**
   * Determine if exceptions should be logged.
   *
   * @return {@code true} if exceptions are logged or {@code false} otherwise
   */
  public boolean isLogExceptions() {
    return logExceptions;
  }

  @Override
  protected void handleJsonValidationFinding(@NonNull JsonValidationFinding finding) {
    Ansi ansi = generatePreamble(finding.getSeverity());

    ansi = ansi.a('[')
        .fgBright(Color.WHITE)
        .a(finding.getCause().getPointerToViolation())
        .reset()
        .a(']');

    URI documentUri = finding.getDocumentUri();
    ansi = documentUri == null
        ? ansi.format(" %s", finding.getMessage())
        : ansi.format(" %s [%s]", finding.getMessage(), documentUri.toString());

    getLogger(finding).log(ansi);
  }

  @Override
  protected void handleXmlValidationFinding(XmlValidationFinding finding) {
    Ansi ansi = generatePreamble(finding.getSeverity());
    SAXParseException ex = finding.getCause();

    URI documentUri = finding.getDocumentUri();
    ansi = documentUri == null
        ? ansi.format("%s [{%d,%d}]",
            finding.getMessage(),
            ex.getLineNumber(),
            ex.getColumnNumber())
        : ansi.format("%s [%s{%d,%d}]",
            finding.getMessage(),
            documentUri.toString(),
            ex.getLineNumber(),
            ex.getColumnNumber());

    getLogger(finding).log(ansi);
  }

  @Override
  protected void handleConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {
    Ansi ansi = generatePreamble(finding.getSeverity());

    ansi.format("[%s]", finding.getTarget().getMetapath());

    String id = finding.getIdentifier();
    if (id != null) {
      ansi.format(" %s:", id);
    }

    getLogger(finding).log(ansi.format(" %s", finding.getMessage()));
  }

  @NonNull
  private LogBuilder getLogger(@NonNull IValidationFinding finding) {
    LogBuilder retval;
    switch (finding.getSeverity()) {
    case CRITICAL:
      retval = LOGGER.atFatal();
      break;
    case ERROR:
      retval = LOGGER.atError();
      break;
    case WARNING:
      retval = LOGGER.atWarn();
      break;
    case INFORMATIONAL:
      retval = LOGGER.atInfo();
      break;
    default:
      throw new IllegalArgumentException("Unknown level: " + finding.getSeverity().name());
    }

    assert retval != null;

    if (finding.getCause() != null && isLogExceptions()) {
      retval.withThrowable(finding.getCause());
    }

    return retval;
  }

  @SuppressWarnings("static-method")
  @NonNull
  private Ansi generatePreamble(@NonNull Level level) {
    Ansi ansi = ansi().fgBright(Color.WHITE).a('[').reset();

    switch (level) {
    case CRITICAL:
      ansi = ansi.fgRed().a("CRITICAL").reset();
      break;
    case ERROR:
      ansi = ansi.fgBrightRed().a("ERROR").reset();
      break;
    case WARNING:
      ansi = ansi.fgBrightYellow().a("WARNING").reset();
      break;
    case INFORMATIONAL:
      ansi = ansi.fgBrightBlue().a("INFO").reset();
      break;
    default:
      ansi = ansi().fgBright(Color.MAGENTA).a(level.name()).reset();
      break;
    }
    ansi = ansi.fgBright(Color.WHITE).a("] ").reset();

    assert ansi != null;
    return ansi;
  }
}