/**
* Copyright 2012 the contributors
*
* 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 com.github.mavenplugins.doctest;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.TreeMap;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributeSet;
import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.doxia.siterenderer.RendererException;
import org.apache.maven.doxia.util.HtmlTools;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicInteger;
/**
* This Mojo reports the doctest results.
*
* @goal report
* @phase site
*/
public class ReportMojo extends AbstractMavenReport {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private static final Pattern JAVADOC_STAR_FINDER = Pattern.compile("^\\s*\\*\\s?", Pattern.MULTILINE);
private static final Pattern JAVADOC_EMPTYLINE_FINDER = Pattern.compile("^\\s*\\*\\s*$", Pattern.MULTILINE);
private static final Pattern ANY_METHOD_FINDER = Pattern.compile("public\\s+void\\s+.*\\s*\\((HttpResponse|"
+ HttpResponse.class.getName().replaceAll("\\.", "\\\\.") + ")", Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE);
private static final SinkEventAttributeSet TABLE_CELL_STYLE_ATTRIBUTES = new SinkEventAttributeSet(new String[] {
"style", "width:150px;" });
private static final String JAVASCRIPT_CODE = "<script type=\"text/javascript\">function toggleVisibility(t){var e=document.getElementById(t);if(e.style.display=='block'){e.style.display='none';}else{e.style.display='block';}}</script>";
/**
* A container which encapsulates endpoints and contains the corresponding doctests.
*/
public class DoctestsContainer {
protected Map<String, DoctestData> doctests = new TreeMap<String, DoctestData>();
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, DoctestData> getDoctests() {
return doctests;
}
public void setDoctests(Map<String, DoctestData> doctests) {
this.doctests = doctests;
}
}
/**
* A container for the doctest data (request, response, javadoc).
*/
public class DoctestData {
protected RequestResultWrapper request;
protected ResponseResultWrapper response;
protected String javaDoc = "";
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJavaDoc() {
return javaDoc;
}
public void setJavaDoc(String javaDoc) {
this.javaDoc = javaDoc;
}
public RequestResultWrapper getRequest() {
return request;
}
public void setRequest(RequestResultWrapper request) {
this.request = request;
}
public ResponseResultWrapper getResponse() {
return response;
}
public void setResponse(ResponseResultWrapper response) {
this.response = response;
}
}
/**
* <i>Maven Internal</i>: The Doxia Site Renderer.
*
* @component
* @required
* @readonly
*/
private Renderer siteRenderer;
/**
* <i>Maven Internal</i>: The Project descriptor.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The number of characters that can be seen without hitting the "more details" button.
*
* @parameter expression="${project.reporting.doctests.maxPreview}" default-value="128"
*/
private int maxPreview = 128;
/**
* the java back-store which has the information where the result are situated.
*/
protected Preferences prefs = Preferences.userNodeForPackage(DoctestRunner.class);
/**
* the report is sorted by endpoint, this map holds them.
*/
protected Map<String, DoctestsContainer> endpoints = new TreeMap<String, DoctestsContainer>();
/**
* the json mapper used to read the doctest results.
*/
protected ObjectMapper mapper = new ObjectMapper();
public int getMaxPreview() {
return maxPreview;
}
public void setMaxPreview(int maxPreview) {
this.maxPreview = maxPreview;
}
@Override
protected String getOutputDirectory() {
return project.getReporting().getOutputDirectory();
}
@Override
public Renderer getSiteRenderer() {
return siteRenderer;
}
@Override
protected MavenProject getProject() {
return project;
}
@Override
public String getOutputName() {
return "doctests/index";
}
@Override
public String getName(Locale locale) {
return getBundle(locale).getString("name");
}
@Override
public String getDescription(Locale locale) {
return getBundle(locale).getString("description");
}
protected ResourceBundle getBundle(Locale locale) {
return ResourceBundle.getBundle("doctest", locale, this.getClass().getClassLoader());
}
public void setSiteRenderer(Renderer siteRenderer) {
this.siteRenderer = siteRenderer;
}
public void setProject(MavenProject project) {
this.project = project;
}
/**
* Parses and renders the doctest results using {@link #parseDoctestResults(File, String)} and {@link #renderDoctestResults(Locale)}.
*/
@Override
protected void executeReport(Locale locale) throws MavenReportException {
File dir;
File results = null;
String doctestResults = "";
try {
prefs.sync();
doctestResults = prefs.get(ReportingCollector.RESULT_PATH, "");
results = new File(doctestResults);
prefs.removeNode();
} catch (BackingStoreException exception) {
getLog().error("error while getting settings", exception);
}
if (!(dir = new File(project.getReporting().getOutputDirectory() + File.separator + "doctests")).exists()) {
dir.mkdirs();
}
if (results != null && results.exists()) {
parseDoctestResults(results, doctestResults);
try {
renderDoctestResults(locale);
} catch (RendererException exception) {
getLog().error("error while rendernig doctests.", exception);
}
}
}
/**
* renders the basic scaffolding via the {@link Sink}. the actual report rendering is done via {@link #renderReport(Sink, Locale)}.
*/
protected void renderDoctestResults(Locale locale) throws RendererException {
Sink sink = getSink();
sink.head();
sink.title();
sink.text(getBundle(locale).getString("header.title"));
sink.title_();
sink.head_();
sink.body();
sink.rawText(JAVASCRIPT_CODE);
renderReport(sink, locale);
sink.body_();
sink.flush();
}
/**
* Iterates through all enpoints and renders all doctest method for each endpoint.
*/
protected void renderReport(Sink sink, Locale locale) {
AtomicInteger counter = new AtomicInteger();
String requestLabel = escapeToHtml(getBundle(locale).getString("request.header"));
String responseLabel = escapeToHtml(getBundle(locale).getString("response.header"));
String detailLabel = escapeToHtml(getBundle(locale).getString("detail.label"));
sink.section1();
sink.sectionTitle1();
sink.text(escapeToHtml(getBundle(locale).getString("toc.title")));
sink.sectionTitle1_();
sink.list();
for (Map.Entry<String, DoctestsContainer> endpoint : endpoints.entrySet()) {
sink.listItem();
sink.anchor(endpoint.getKey());
sink.text(endpoint.getKey());
sink.anchor_();
sink.listItem_();
}
sink.list_();
for (Map.Entry<String, DoctestsContainer> endpoint : endpoints.entrySet()) {
sink.section2();
sink.sectionTitle2();
sink.text(endpoint.getKey());
sink.sectionTitle2_();
for (Map.Entry<String, DoctestData> doctest : endpoint.getValue().getDoctests().entrySet()) {
sink.section3();
sink.sectionTitle3();
sink.text(doctest.getKey());
sink.sectionTitle3_();
if (!StringUtils.isEmpty(doctest.getValue().getJavaDoc())) {
sink.verbatim(SinkEventAttributeSet.BOXED);
sink.rawText(doctest.getValue().getJavaDoc());
sink.verbatim_();
}
sink.table();
sink.tableRow();
sink.tableCell(TABLE_CELL_STYLE_ATTRIBUTES);
sink.bold();
sink.text(requestLabel);
sink.bold_();
sink.tableCell_();
sink.tableCell();
renderRequestCell(sink, doctest.getValue().getRequest(), counter, detailLabel);
sink.tableCell_();
sink.tableRow_();
sink.tableRow();
sink.tableCell(TABLE_CELL_STYLE_ATTRIBUTES);
sink.bold();
sink.text(responseLabel);
sink.bold_();
sink.tableCell_();
sink.tableCell();
renderResponseCell(sink, doctest.getValue().getResponse(), counter, detailLabel);
sink.tableCell_();
sink.tableRow_();
sink.table_();
sink.section3_();
}
sink.section2_();
}
sink.section1_();
}
/**
* Renders the request cell in the table
*/
protected void renderRequestCell(Sink sink, RequestResultWrapper wrapper, AtomicInteger counter, String details) {
StringBuilder builder = new StringBuilder();
String preview;
int id = counter.incrementAndGet();
builder.append(wrapper.getRequestLine());
builder.append("<br/>");
builder.append("<a href=\"javascript:\" onclick=\"toggleVisibility('request-detail-");
builder.append(id);
builder.append("');toggleVisibility('request-detail-");
builder.append(id);
builder.append("-preview');\">");
builder.append(details);
builder.append("</a><br/><div id=\"request-detail-");
builder.append(id);
builder.append("-preview\" style=\"display: block;\">");
sink.rawText(builder.toString());
builder.delete(0, builder.length());
preview = wrapper.getEntity();
if (!StringUtils.isEmpty(wrapper.getEntity()) && wrapper.getEntity().length() <= maxPreview) {
preview = wrapper.getEntity();
} else if (!StringUtils.isEmpty(wrapper.getEntity())) {
preview = wrapper.getEntity().substring(0, maxPreview) + "…";
}
if (!StringUtils.isEmpty(wrapper.getEntity())) {
sink.verbatim(SinkEventAttributeSet.BOXED);
sink.rawText(preview);
sink.verbatim_();
}
builder.append("</div>");
builder.append("<div id=\"request-detail-");
builder.append(id);
builder.append("\" style=\"display: none;\">");
sink.rawText(builder.toString());
builder.delete(0, builder.length());
if (wrapper.getHeader() != null && wrapper.getHeader().length > 0) {
sink.verbatim(SinkEventAttributeSet.BOXED);
for (String header : wrapper.getHeader()) {
sink.rawText(header);
sink.rawText("<br/>");
}
sink.verbatim_();
}
if (wrapper.getParemeters() != null && wrapper.getParemeters().length > 0) {
sink.verbatim(SinkEventAttributeSet.BOXED);
for (String parameter : wrapper.getParemeters()) {
sink.rawText(parameter);
sink.rawText("<br/>");
}
sink.verbatim_();
}
if (!StringUtils.isEmpty(wrapper.getEntity())) {
sink.verbatim(SinkEventAttributeSet.BOXED);
sink.rawText(wrapper.getEntity());
sink.verbatim_();
}
sink.rawText("</div>");
}
/**
* Renders the response cell in the table.
*/
protected void renderResponseCell(Sink sink, ResponseResultWrapper wrapper, AtomicInteger counter, String details) {
StringBuilder builder = new StringBuilder();
String preview;
int id = counter.incrementAndGet();
builder.append(wrapper.getStatusLine());
builder.append("<br/>");
builder.append("<a href=\"javascript:\" onclick=\"toggleVisibility('response-detail-");
builder.append(id);
builder.append("');toggleVisibility('response-detail-");
builder.append(id);
builder.append("-preview');\">");
builder.append(details);
builder.append("</a><br/><div id=\"response-detail-");
builder.append(id);
builder.append("-preview\" style=\"display: block;\">");
sink.rawText(builder.toString());
builder.delete(0, builder.length());
preview = wrapper.getEntity();
if (!StringUtils.isEmpty(wrapper.getEntity()) && wrapper.getEntity().length() <= maxPreview) {
preview = wrapper.getEntity();
} else if (!StringUtils.isEmpty(wrapper.getEntity())) {
preview = wrapper.getEntity().substring(0, maxPreview) + "…";
}
if (!StringUtils.isEmpty(wrapper.getEntity())) {
sink.verbatim(SinkEventAttributeSet.BOXED);
sink.rawText(preview);
sink.verbatim_();
}
builder.append("</div>");
builder.append("<div id=\"response-detail-");
builder.append(id);
builder.append("\" style=\"display: none;\">");
sink.rawText(builder.toString());
builder.delete(0, builder.length());
if (wrapper.getHeader() != null && wrapper.getHeader().length > 0) {
sink.verbatim(SinkEventAttributeSet.BOXED);
for (String header : wrapper.getHeader()) {
sink.rawText(header);
sink.rawText("<br/>");
}
sink.verbatim_();
}
if (wrapper.getParemeters() != null && wrapper.getParemeters().length > 0) {
sink.verbatim(SinkEventAttributeSet.BOXED);
for (String parameter : wrapper.getParemeters()) {
sink.rawText(parameter);
sink.rawText("<br/>");
}
sink.verbatim_();
}
if (!StringUtils.isEmpty(wrapper.getEntity())) {
sink.verbatim(SinkEventAttributeSet.BOXED);
sink.rawText(wrapper.getEntity());
sink.verbatim_();
}
sink.rawText("</div>");
}
/**
* Gets the doctest results and transforms them into {@link DoctestsContainer} objects.
*/
protected void parseDoctestResults(File doctestResultDirectory, String doctestResultDirectoryName) {
String tmp;
String key;
String className;
String doctestName;
String requestDataClass;
String sourceName;
String source;
DoctestsContainer endpoint;
DoctestData doctest;
RequestResultWrapper requestResult;
ResponseResultWrapper responseResult;
Map<String, RequestResultWrapper> requestResults = new HashMap<String, RequestResultWrapper>();
Map<String, ResponseResultWrapper> responseResults = new HashMap<String, ResponseResultWrapper>();
ZipInputStream zipInputStream;
ZipEntry zipEntry;
for (File resultFile : FileUtils.listFiles(doctestResultDirectory, new String[] { "zip" }, false)) {
zipInputStream = null;
try {
zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(resultFile)));
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
tmp = zipEntry.getName();
className = getClassName(tmp);
doctestName = getDoctestName(tmp);
requestDataClass = getRequestDataClass(tmp);
sourceName = getSourceName(className);
key = getKey(tmp);
if (isRequest(tmp)) {
requestResults.put(key, mapper.readValue(new FilterInputStream(zipInputStream) {
@Override
public void close() throws IOException {
}
}, RequestResultWrapper.class));
} else if (isResponse(tmp)) {
responseResults.put(key, mapper.readValue(new FilterInputStream(zipInputStream) {
@Override
public void close() throws IOException {
}
}, ResponseResultWrapper.class));
}
if (requestResults.containsKey(key) && responseResults.containsKey(key)) {
try {
requestResult = requestResults.get(key);
responseResult = responseResults.get(key);
requestResults.remove(key);
responseResults.remove(key);
source = FileUtils.readFileToString(new File(project.getBuild().getTestSourceDirectory(),
sourceName));
tmp = className + '.' + doctestName;
endpoint = endpoints.get(requestResult.getPath());
if (endpoint == null) {
endpoint = new DoctestsContainer();
endpoint.setName(requestResult.getPath());
endpoints.put(requestResult.getPath(), endpoint);
}
requestResult.setEntity(escapeToHtml(requestResult.getEntity()));
requestResult.setPath(escapeToHtml(requestResult.getPath()));
requestResult.setRequestLine(escapeToHtml(requestResult.getRequestLine()));
requestResult.setHeader(escapeToHtml(requestResult.getHeader()));
requestResult.setParemeters(escapeToHtml(requestResult.getParemeters()));
responseResult.setEntity(escapeToHtml(responseResult.getEntity()));
responseResult.setStatusLine(escapeToHtml(responseResult.getStatusLine()));
responseResult.setHeader(escapeToHtml(responseResult.getHeader()));
responseResult.setParemeters(escapeToHtml(responseResult.getParemeters()));
doctest = new DoctestData();
doctest.setJavaDoc(getJavaDoc(source, doctestName));
doctest.setName(tmp);
doctest.setRequest(requestResult);
doctest.setResponse(responseResult);
endpoint.getDoctests().put(tmp, doctest);
} catch (IOException exception) {
getLog().error("error while reading doctest request", exception);
}
}
}
} catch (IOException exception) {
getLog().error("error while reading doctest request", exception);
} finally {
if (zipInputStream != null) {
try {
zipInputStream.close();
} catch (IOException exception) {
getLog().error("error while reading doctest request", exception);
}
}
}
}
}
private String getSourceName(String className) {
return className.replaceAll("\\.", "/") + ".java";
}
private String getKey(String tmp) {
return tmp.substring(0, tmp.lastIndexOf('.'));
}
private boolean isResponse(String tmp) {
return tmp.endsWith(".response");
}
private boolean isRequest(String tmp) {
return tmp.endsWith(".request");
}
private String getRequestDataClass(String tmp) {
return tmp.substring(tmp.lastIndexOf('-') + 1, tmp.lastIndexOf('.'));
}
private String getDoctestName(String tmp) {
return tmp.substring(tmp.indexOf('-') + 1, tmp.lastIndexOf('-'));
}
private String getClassName(String tmp) {
return tmp.substring(0, tmp.indexOf('-'));
}
/**
* Gets the javadoc comment situated over a doctest method.
*/
protected String getJavaDoc(String source, String method) {
Pattern methodPattern = Pattern.compile("public\\s+void\\s+" + method + "\\s*\\((HttpResponse|"
+ HttpResponse.class.getName().replaceAll("\\.", "\\\\.") + ")", Pattern.CASE_INSENSITIVE
| Pattern.MULTILINE);
Matcher matcher = methodPattern.matcher(source);
int start, tmp, last, comment;
String doc;
if (matcher.find()) {
start = matcher.start();
last = -1;
matcher = ANY_METHOD_FINDER.matcher(source);
while (matcher.find() && (tmp = matcher.start()) < start) {
last = tmp;
}
comment = source.lastIndexOf("/**", start);
if (comment > 2 && (comment > last || last == -1)) {
doc = source.substring(comment, source.indexOf("*/", comment));
doc = doc.substring(3, doc.length() - 2);
doc = JAVADOC_EMPTYLINE_FINDER.matcher(doc).replaceAll(LINE_SEPARATOR);
doc = JAVADOC_STAR_FINDER.matcher(doc).replaceAll("");
doc = StringUtils.replace(doc, " ", " ");
doc = StringUtils.replace(doc, LINE_SEPARATOR, "<br/>");
return doc;
}
}
return "";
}
/**
* Escapes an array of strings.
*/
protected String[] escapeToHtml(String[] texts) {
for (int i = 0; i < texts.length; i++) {
texts[i] = escapeToHtml(texts[i]);
}
return texts;
}
/**
* Escapes a single string.
*/
protected String escapeToHtml(String text) {
return StringUtils.replace(StringUtils.replace(HtmlTools.escapeHTML(text, false), "&#", ""),
LINE_SEPARATOR, "<br/>");
}
}