JsonReporter.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.invoker.mvnval;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.services.ModelProblem;
/**
* Renders validation results as a JSON array, one object per validated file. Assembled by hand
* so the tool needs no dependency the distribution does not already carry.
*/
class JsonReporter {
private static final char[] HEX = "0123456789abcdef".toCharArray();
private static final char FIRST_PRINTABLE_ASCII = 0x20;
private static final char LAST_PRINTABLE_ASCII = 0x7e;
/** The two quotes, plus room for a few escapes. */
private static final int ESCAPE_HEADROOM = 16;
static void report(List<Report> reports, Consumer<String> out) {
out.accept(reports.stream().map(JsonReporter::toJson).collect(Collectors.joining(",", "[", "]")));
}
private static String toJson(Report report) {
StringBuilder sb =
new StringBuilder("{\"pom\":").append(quote(report.pom().toString()));
if (report.failure() != null) {
sb.append(",\"failure\":").append(quote(report.failure()));
}
return sb.append(",\"problems\":")
.append(report.problems().stream()
.map(problem -> toJson(problem, report))
.collect(Collectors.joining(",", "[", "]")))
.append('}')
.toString();
}
private static String toJson(ModelProblem problem, Report report) {
StringBuilder sb = new StringBuilder("{\"severity\":")
.append(quote(problem.getSeverity().name()))
.append(",\"message\":")
.append(quote(problem.getMessage()));
// Absent values are left out, not written as the -1 the model builder uses for "no location".
String source = report.sourceOf(problem);
if (source != null) {
sb.append(",\"source\":").append(quote(source));
}
if (problem.getLineNumber() > 0) {
sb.append(",\"line\":").append(problem.getLineNumber());
}
if (problem.getColumnNumber() > 0) {
sb.append(",\"column\":").append(problem.getColumnNumber());
}
return sb.append('}').toString();
}
private static String quote(@Nullable String value) {
if (value == null) {
return "null";
}
StringBuilder sb = null;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
String escaped = escaped(c);
if (escaped == null) {
if (sb != null) {
sb.append(c);
}
} else {
if (sb == null) {
sb = new StringBuilder(value.length() + ESCAPE_HEADROOM)
.append('"')
.append(value, 0, i);
}
sb.append(escaped);
}
}
return sb == null ? '"' + value + '"' : sb.append('"').toString();
}
/**
* The replacement for a character that cannot stand for itself, or {@code null} when it can.
* <p>
* Everything outside printable ASCII is escaped, not just the control characters JSON forbids:
* the writer's charset is the console's, which on Windows is routinely not UTF-8, and a path
* can carry an unpaired surrogate that no charset can encode.
*/
@Nullable
private static String escaped(char c) {
return switch (c) {
case '"' -> "\\\"";
case '\\' -> "\\\\";
case '\n' -> "\\n";
case '\r' -> "\\r";
case '\t' -> "\\t";
default ->
c < FIRST_PRINTABLE_ASCII || c > LAST_PRINTABLE_ASCII
? new String(new char[] {
'\\', 'u', HEX[c >> 12 & 0xf], HEX[c >> 8 & 0xf], HEX[c >> 4 & 0xf], HEX[c & 0xf]
})
: null;
};
}
}