package net.thucydides.core.reports.html;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Key;
import net.thucydides.core.ThucydidesSystemProperty;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.issues.IssueTracking;
import net.thucydides.core.reports.renderer.Asciidoc;
import net.thucydides.core.reports.renderer.MarkupRenderer;
import net.thucydides.core.util.EnvironmentVariables;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.translate.AggregateTranslator;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.EntityArrays;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ch.lambdaj.Lambda.join;
/**
* Format text for HTML reports.
* In particular, this integrates JIRA links into the generated reports.
*/
public class Formatter {
private final static String ISSUE_NUMBER_REGEXP = "#([A-Z][A-Z0-9-_]*)?-?\\d+";
private final static Pattern shortIssueNumberPattern = Pattern.compile(ISSUE_NUMBER_REGEXP);
private final static String FULL_ISSUE_NUMBER_REGEXP = "([A-Z][A-Z0-9-_]*)-\\d+";
private final static Pattern fullIssueNumberPattern = Pattern.compile(FULL_ISSUE_NUMBER_REGEXP);
private final static String ISSUE_LINK_FORMAT = "<a target=\"_blank\" href=\"{0}\">{1}</a>";
private static final String ELIPSE = "…";
private static final String ASCIIDOC = "asciidoc";
private static final String NEW_LINE = System.getProperty("line.separator");
private final IssueTracking issueTracking;
private final EnvironmentVariables environmentVariables;
private final MarkupRenderer asciidocRenderer;
@Inject
public Formatter(IssueTracking issueTracking, EnvironmentVariables environmentVariables) {
this.issueTracking = issueTracking;
this.environmentVariables = environmentVariables;
this.asciidocRenderer = Injectors.getInjector().getInstance(Key.get(MarkupRenderer.class, Asciidoc.class));
}
public Formatter(IssueTracking issueTracking) {
this(issueTracking, Injectors.getInjector().getProvider(EnvironmentVariables.class).get() );
}
public String renderAsciidoc(String text) {
return stripNewLines(asciidocRenderer.render(text));
}
private String stripNewLines(String render) {
return render.replaceAll("\n","");
}
public String stripQualifications(String title) {
if (title.contains("[")) {
return title.substring(0,title.lastIndexOf("["));
} else {
return title;
}
}
static class IssueExtractor {
private String workingCopy;
IssueExtractor(String initialValue) {
this.workingCopy = initialValue;
}
public List<String> getShortenedIssues() {
Matcher matcher = shortIssueNumberPattern.matcher(workingCopy);
ArrayList<String> issues = Lists.newArrayList();
while (matcher.find()) {
String issue = matcher.group();
issues.add(issue);
workingCopy = workingCopy.replaceFirst(issue, "");
}
return issues;
}
public List<String> getFullIssues() {
Matcher unhashedMatcher = fullIssueNumberPattern.matcher(workingCopy);
ArrayList<String> issues = Lists.newArrayList();
while (unhashedMatcher.find()) {
String issue = unhashedMatcher.group();
issues.add(issue);
workingCopy = workingCopy.replaceFirst(issue, "");
}
return issues;
}
}
public static List<String> issuesIn(final String value) {
IssueExtractor extractor = new IssueExtractor(value);
List<String> issuesWithHash = extractor.getShortenedIssues();
List<String> allIssues = extractor.getFullIssues();
allIssues.addAll(issuesWithHash);
return allIssues;
}
public String addLinks(final String value) {
if (issueTracking == null) {
return value;
}
String formattedValue = value;
if (issueTracking.getIssueTrackerUrl() != null) {
formattedValue = insertFullIssueTrackingUrls(value);
}
if (issueTracking.getShortenedIssueTrackerUrl() != null) {
formattedValue = insertShortenedIssueTrackingUrls(formattedValue);
}
return formattedValue;
}
public String renderDescription(final String text) {
String format = environmentVariables.getProperty(ThucydidesSystemProperty.NARRATIVE_FORMAT,"");
if (isRenderedHtml(text)) {
return text;
} else if (format.equalsIgnoreCase(ASCIIDOC)) {
return renderAsciidoc(text);
} else {
return addLineBreaks(text);
}
}
private boolean isRenderedHtml(String text) {
return (text != null) && (text.startsWith("<"));
}
public String addLineBreaks(final String text) {
return (text != null) ?
text.replaceAll(IOUtils.LINE_SEPARATOR_WINDOWS, "<br>").replaceAll(IOUtils.LINE_SEPARATOR_UNIX, "<br>") : "";
}
public String convertAnyTables(String text) {
text = convertNonStandardNLChars(text);
if (shouldFormatEmbeddedTables() && containsEmbeddedTable(text)) {
text = withTablesReplaced(text);
}
return text;
}
private String withTablesReplaced(String text) {
List<String> unformattedTables = getEmbeddedTablesIn(text);
for(String unformattedTable : unformattedTables) {
// String unformattedTable = getFirstEmbeddedTable(text);
ExampleTable table = new ExampleTable(unformattedTable);
text = text.replace(unformattedTable, table.inHtmlFormat());
}
text = text.replaceAll(newLineUsedIn(text), "<br>");
return text;
}
private String convertNonStandardNLChars(String text) {
text = StringUtils.replace(text, "\r", NEW_LINE);
return StringUtils.replace(text, "", NEW_LINE);
}
private boolean shouldFormatEmbeddedTables() {
return !environmentVariables.getPropertyAsBoolean(ThucydidesSystemProperty.IGNORE_EMBEDDED_TABLES, false);
}
private boolean containsEmbeddedTable(String text) {
return ((positionOfFirstPipeIn(text) >= 0) && (positionOfLastPipeIn(text) >= 0));
}
private int positionOfLastPipeIn(String text) {
return text.indexOf("|", positionOfFirstPipeIn(text) + 1);
}
private int positionOfFirstPipeIn(String text) {
return text.indexOf("|");
}
private List<String> getEmbeddedTablesIn(String text) {
List<String> embeddedTables = Lists.newArrayList();
BufferedReader reader = new BufferedReader(new StringReader(text));
StringBuffer tableText = new StringBuffer();
boolean inTable = false;
String newLine = newLineUsedIn(text);
try {
String line;
while ((line = reader.readLine()) != null) {
if (!inTable && line.contains("|")){ // start of a table
inTable = true;
} else if (inTable && !line.contains("|") && !(isBlank(line))){ // end of a table
embeddedTables.add(tableText.toString().trim());
tableText = new StringBuffer();
inTable = false;
}
if (inTable) {
tableText.append(line).append(newLine);
}
}
} catch (IOException e) {
throw new IllegalArgumentException("Could not process embedded table", e);
}
if (!tableText.toString().isEmpty()) {
embeddedTables.add(tableText.toString().trim());
}
return embeddedTables;
}
private String getFirstEmbeddedTable(String text) {
BufferedReader reader = new BufferedReader(new StringReader(text));
StringBuffer tableText = new StringBuffer();
boolean inTable = false;
String newLine = newLineUsedIn(text);
try {
String line;
while ((line = reader.readLine()) != null) {
if (!inTable && line.contains("|")){
inTable = true;
} else if (inTable && !line.contains("|") && !(isBlank(line))){
break;
}
if (inTable) {
tableText.append(line).append(newLine);
}
}
} catch (IOException e) {
throw new IllegalArgumentException("Could not process embedded table", e);
}
return tableText.toString().trim();
}
private boolean isBlank(String line) {
return (StringUtils.isBlank(line.trim()));
}
private String newLineUsedIn(String text) {
if (text.contains("\r\n")) {
return "\r\n";
} else if (text.contains("\n")) {
return "\n";
} else if (text.contains("\r")) {
return "\r";
} else {
return NEW_LINE;
}
}
private final CharSequenceTranslator ESCAPE_SPECIAL_CHARS = new AggregateTranslator(
new LookupTranslator(EntityArrays.ISO8859_1_ESCAPE()),
new LookupTranslator(EntityArrays.HTML40_EXTENDED_ESCAPE())
);
public String htmlCompatible(Object fieldValue) {
return addLineBreaks(ESCAPE_SPECIAL_CHARS.translate(fieldValue != null ? stringFormOf(fieldValue) : ""));
}
private String stringFormOf(Object fieldValue) {
if (Iterable.class.isAssignableFrom(fieldValue.getClass())) {
return "[" + join(fieldValue) + "]";
} else {
return fieldValue.toString();
}
}
public String truncatedHtmlCompatible(String text, int length) {
return addLineBreaks(ESCAPE_SPECIAL_CHARS.translate(truncate(text, length)));
}
private String truncate(String text, int length) {
if (text.length() > length) {
return text.substring(0, length).trim() + ELIPSE;
} else {
return text;
}
}
private String replaceWithTokens(String value, List<String> issues) {
List<String> sortedIssues = inOrderOfDecreasingLength(issues);
for(int i = 0; i < sortedIssues.size(); i++) {
value = value.replaceAll(sortedIssues.get(i), "%%%" + i + "%%%");
}
return value;
}
private List<String> inOrderOfDecreasingLength(List<String> issues) {
List<String> sortedIssues = Lists.newArrayList(issues);
Collections.sort(sortedIssues, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.length() - o1.length();
}
});
return sortedIssues;
}
public static List<String> shortenedIssuesIn(String value) {
IssueExtractor extractor = new IssueExtractor(value);
return extractor.getShortenedIssues();
}
public static List<String> fullIssuesIn(String value) {
IssueExtractor extractor = new IssueExtractor(value);
return extractor.getFullIssues();
}
private String insertShortenedIssueTrackingUrls(String value) {
String issueUrlFormat = issueTracking.getShortenedIssueTrackerUrl();
List<String> issues = sortByDecreasingSize(shortenedIssuesIn(value));
String formattedValue = replaceWithTokens(value, issues);
int i = 0;
for (String issue : issues) {
String issueUrl = MessageFormat.format(issueUrlFormat, stripLeadingHashFrom(issue));
String issueLink = MessageFormat.format(ISSUE_LINK_FORMAT, issueUrl, issue);
String token = "%%%" + i++ + "%%%";
formattedValue = formattedValue.replaceAll(token, issueLink);
}
return formattedValue;
}
private String insertFullIssueTrackingUrls(String value) {
String issueUrlFormat = issueTracking.getIssueTrackerUrl();
List<String> issues = sortByDecreasingSize(fullIssuesIn(value));
String formattedValue = replaceWithTokens(value, issues);
int i = 0;
for (String issue : issues) {
String issueUrl = MessageFormat.format(issueUrlFormat, issue);
String issueLink = MessageFormat.format(ISSUE_LINK_FORMAT, issueUrl, issue);
String token = "%%%" + i++ + "%%%";
formattedValue = formattedValue.replaceAll(token, issueLink);
}
return formattedValue;
}
private List<String> sortByDecreasingSize(List<String> issues) {
List<String> sortedIssues = Lists.newArrayList(issues);
Collections.sort(sortedIssues, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return new Integer(-a.length()).compareTo(new Integer(b.length()));
}
});
return sortedIssues;
}
public String formatWithFields(String textToFormat, List<String> fields) {
String textWithEscapedFields = textToFormat;
for (String field : fields) {
textWithEscapedFields = textWithEscapedFields.replaceAll("<" + field + ">", "<" + field + ">");
}
return addLineBreaks(removeMacros(convertAnyTables(textWithEscapedFields)));
}
private String removeMacros(String textToFormat) {
return textToFormat.replaceAll("\\{trim=false\\}\\s*\\r?\\n","");
}
private String stripLeadingHashFrom(final String issue) {
return issue.substring(1);
}
}