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