/*
* Copyright 2015 Lukas Krejci
*
* Licensed 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.revapi.reporter.text;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.revapi.AnalysisContext;
import org.revapi.Difference;
import org.revapi.DifferenceSeverity;
import org.revapi.Element;
import org.revapi.Report;
import org.revapi.Reporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.Template;
import freemarker.template.TemplateException;
/**
* @author Lukas Krejci
* @since 0.1
*/
public class TextReporter implements Reporter {
private static final Logger LOG = LoggerFactory.getLogger(TextReporter.class);
private static final String CONFIG_ROOT_PATH = "revapi.reporter.text";
private DifferenceSeverity minLevel;
private PrintWriter output;
private boolean shouldClose;
private SortedSet<Report> reports;
private Template template;
private AnalysisContext analysis;
@Nullable
@Override
public String[] getConfigurationRootPaths() {
return new String[]{CONFIG_ROOT_PATH};
}
@Nullable
@Override
public Reader getJSONSchema(@Nonnull String configurationRootPath) {
if (CONFIG_ROOT_PATH.equals(configurationRootPath)) {
return new InputStreamReader(getClass().getResourceAsStream("/META-INF/schema.json"),
Charset.forName("UTF-8"));
} else {
return null;
}
}
/**
* For testing.
*
* @param wrt the output writer
*/
void setOutput(PrintWriter wrt) {
this.output = wrt;
this.shouldClose = true;
}
@Override
public void initialize(@Nonnull AnalysisContext analysis) {
//noinspection ConstantConditions
if (analysis != null) {
try {
flushReports();
} catch (IOException e) {
throw new IllegalStateException("Failed to output previous analysis report.");
}
}
this.analysis = analysis;
String minLevel = analysis.getConfiguration().get("revapi", "reporter", "text", "minSeverity").asString();
String output = analysis.getConfiguration().get("revapi", "reporter", "text", "output").asString();
output = "undefined".equals(output) ? "out" : output;
String templatePath = analysis.getConfiguration().get("revapi", "reporter", "text", "template").asString();
if ("undefined".equals(templatePath)) {
templatePath = null;
}
boolean append = analysis.getConfiguration().get("revapi", "reporter", "text", "append").asBoolean(false);
this.minLevel = "undefined".equals(minLevel) ? DifferenceSeverity.POTENTIALLY_BREAKING :
DifferenceSeverity.valueOf(minLevel);
OutputStream out;
switch (output) {
case "out":
out = System.out;
break;
case "err":
out = System.err;
break;
default:
File f = new File(output);
if (f.exists() && !f.canWrite()) {
LOG.warn(
"The configured file for text reporter, '" + f.getAbsolutePath() + "' is not a writable file." +
" Defaulting the output to standard output.");
out = System.out;
} else {
File parent = f.getParentFile();
if (!parent.exists()) {
if (!parent.mkdirs()) {
LOG.warn("Failed to create directory structure to write to the configured output file '" +
f.getAbsolutePath() + "'. Defaulting the output to standard output.");
out = System.out;
break;
}
}
try {
out = new FileOutputStream(output, append);
} catch (FileNotFoundException e) {
LOG.warn("Failed to create the configured output file '" + f.getAbsolutePath() + "'." +
" Defaulting the output to standard output.", e);
out = System.out;
}
}
}
shouldClose = out != System.out && out != System.err;
this.output = new PrintWriter(new OutputStreamWriter(out, Charset.forName("UTF-8")));
this.reports = new TreeSet<>((r1, r2) -> {
Element r1El = r1.getOldElement() == null ? r1.getNewElement() : r1.getOldElement();
Element r2El = r2.getOldElement() == null ? r2.getNewElement() : r2.getOldElement();
//noinspection ConstantConditions
return r1El.compareTo(r2El);
});
Configuration freeMarker = createFreeMarkerConfiguration();
template = null;
try {
template = templatePath == null
? freeMarker.getTemplate("default-template-with-improbable-name@@#(*&$)(.ftl")
: freeMarker.getTemplate(templatePath);
} catch (IOException e) {
throw new IllegalStateException("Failed to initialize the freemarker template.", e);
}
}
/**
* Creates a new FreeMarker configuration.
* By default, it is configured as follows:
* <ul>
* <li>compatibility level is set to 2.3.23
* <li>the object wrapper is configured to expose fields
* <li>API builtins are enabled
* <li>there are 2 template loaders - 1 for loading templates from /META-INF using a classloader and a second
* one to load templates from files.
* </ul>
* @return
*/
protected Configuration createFreeMarkerConfiguration() {
DefaultObjectWrapperBuilder bld = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_23);
bld.setExposeFields(true);
Configuration freeMarker = new Configuration(Configuration.VERSION_2_3_23);
freeMarker.setObjectWrapper(bld.build());
freeMarker.setAPIBuiltinEnabled(true);
freeMarker.setTemplateLoader(new MultiTemplateLoader(
new TemplateLoader[]{new ClassTemplateLoader(getClass(), "/META-INF"),
new NaiveFileTemplateLoader()}));
return freeMarker;
}
@Override
public void report(@Nonnull Report report) {
if (report.getDifferences().isEmpty()) {
return;
}
DifferenceSeverity maxReportedSeverity = DifferenceSeverity.NON_BREAKING;
for (Difference d : report.getDifferences()) {
for (DifferenceSeverity c : d.classification.values()) {
if (c.compareTo(maxReportedSeverity) > 0) {
maxReportedSeverity = c;
}
}
}
if (maxReportedSeverity.compareTo(minLevel) < 0) {
return;
}
reports.add(report);
}
@Override
public void close() throws IOException {
flushReports();
if (shouldClose) {
output.close();
}
}
private void flushReports() throws IOException {
try {
if (output != null && template != null) {
HashMap<String, Object> root = new HashMap<>();
root.put("reports", reports);
root.put("analysis", analysis);
template.process(root, output);
}
} catch (TemplateException e) {
throw new IOException("Failed to output the reports.", e);
}
}
}