1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.util;
7   
8   import static org.fusesource.jansi.Ansi.ansi;
9   
10  import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
11  import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
12  import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor;
13  import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
14  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
15  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
16  import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler;
17  
18  import org.apache.logging.log4j.LogBuilder;
19  import org.apache.logging.log4j.LogManager;
20  import org.apache.logging.log4j.Logger;
21  import org.fusesource.jansi.Ansi;
22  import org.fusesource.jansi.Ansi.Color;
23  import org.xml.sax.SAXParseException;
24  
25  import java.net.URI;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
31  
32  /**
33   * Supports logging validation findings to the console using ANSI color codes to
34   * improve the visibility of warnings and errors.
35   */
36  public final class LoggingValidationHandler
37      extends AbstractValidationResultProcessor {
38    private static final Logger LOGGER = LogManager.getLogger(LoggingValidationHandler.class);
39  
40    @NonNull
41    private static final LoggingValidationHandler NO_LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(false);
42    @NonNull
43    private static final LoggingValidationHandler LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(true);
44  
45    private final boolean logExceptions;
46  
47    /**
48     * Get a singleton instance of the logging validation handler.
49     * <p>
50     * This instance will not log exceptions.
51     *
52     * @return the instance
53     */
54    @NonNull
55    public static LoggingValidationHandler instance() {
56      return instance(false);
57    }
58  
59    /**
60     * Get a singleton instance of the logging validation handler.
61     *
62     * @param logExceptions
63     *          {@code true} if this instance will log exceptions or {@code false}
64     *          otherwise
65     * @return the instance
66     */
67    @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED",
68        justification = "both values are class initialized")
69    @NonNull
70    public static LoggingValidationHandler instance(boolean logExceptions) {
71      return logExceptions ? LOG_EXCPTION_INSTANCE : NO_LOG_EXCPTION_INSTANCE;
72    }
73  
74    private LoggingValidationHandler(boolean logExceptions) {
75      this.logExceptions = logExceptions;
76    }
77  
78    /**
79     * Determine if exceptions should be logged.
80     *
81     * @return {@code true} if exceptions are logged or {@code false} otherwise
82     */
83    public boolean isLogExceptions() {
84      return logExceptions;
85    }
86  
87    @Override
88    protected void handleJsonValidationFinding(@NonNull JsonValidationFinding finding) {
89      Ansi ansi = generatePreamble(finding.getSeverity());
90  
91      ansi = ansi.a('[')
92          .fgBright(Color.WHITE)
93          .a(finding.getCause().getPointerToViolation())
94          .reset()
95          .a(']');
96  
97      URI documentUri = finding.getDocumentUri();
98      ansi = documentUri == null
99          ? 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 }