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}