package cucumber.contrib.formatter.pdf; import com.google.common.collect.Lists; import com.google.common.io.ByteSource; import com.itextpdf.text.*; import com.itextpdf.text.pdf.*; import com.itextpdf.text.pdf.draw.LineSeparator; import cucumber.contrib.formatter.FormatterException; import cucumber.contrib.formatter.model.*; import cucumber.contrib.formatter.util.BricABrac; import cucumber.contrib.util.Provider; import gherkin.formatter.model.DataTableRow; import gherkin.formatter.model.DocString; import gherkin.formatter.model.Row; import gherkin.formatter.model.Tag; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import static com.google.common.io.Resources.asByteSource; import static cucumber.contrib.formatter.pdf.Configuration.extendTableToWidth; public class PdfEmitter implements Provider<PdfWriter> { private Logger log = LoggerFactory.getLogger(PdfEmitter.class); private Document document; private Configuration configuration; private RootStatistics statistics; private boolean firstFeature = true; // private File fileTmp; private File fileDst; private PdfWriter pdfWriter; private FileOutputStream fileOutputStream; // private HeaderFooter headerFooter; private TableOfContents tableOfContents; private PageNumber pageNumber; public PdfEmitter(Configuration configuration) { this.configuration = configuration; initStatistics(); } private void initStatistics() { this.statistics = new RootStatistics(new RootStatistics.Filter() { @Override public boolean isManual(ScenarioWrapper scenarioWrapper) { return scenarioWrapper.hasTag(configuration.manualTag()); } }); } @Override public PdfWriter get() { return pdfWriter; } public void init(File fileDst) throws FileNotFoundException, DocumentException { this.fileDst = fileDst; this.fileTmp = new File(fileDst.getParentFile(), fileDst.getName() + ".tmp"); this.document = configuration.createDocument(); this.fileOutputStream = new FileOutputStream(fileTmp); this.pageNumber = configuration.getPageNumber(); this.tableOfContents = configuration.getTableOfContent(); this.headerFooter = configuration.createHeaderFooter(); // pdfWriter = PdfWriter.getInstance(document, fileOutputStream); pdfWriter.setBoxSize("art", configuration.getDocumentArtBox()); pdfWriter.setPageEvent(headerFooter); pdfWriter.setPageEvent(tableOfContents); document.open(); } public Document getDocument() { return document; } private void writeInitialData() { try { pageNumber.startContent(); configuration.writeMetaInformations(document); configuration.writeFirstPages(document); configuration.writePreambule(document); } catch (DocumentException e) { log.warn("Error while writing initial data", e); } } public void emit(FeatureWrapper feature) { if (firstFeature) { firstFeature = false; writeInitialData(); } // statistics statistics.consolidate(feature); // // Title Chapter featureChap = configuration.createTitledChapter(feature.getName()); // Uri if (configuration.shouldDisplayUri()) { Paragraph uri = new Paragraph("Uri: " + feature.getUri(), configuration.defaultMetaFont()); featureChap.add(uri); } // Description String description = feature.getDescription(); if (BricABrac.isNotBlank(description)) { Margin descriptionMargin = configuration.getDescriptionMargin(); Paragraph paragraph = new Paragraph("", configuration.defaultFont()); paragraph.setSpacingBefore(descriptionMargin.marginTop); paragraph.setSpacingAfter(descriptionMargin.marginBottom); paragraph.setIndentationLeft(descriptionMargin.marginLeft); configuration.appendMarkdownContent(paragraph, description); featureChap.add(paragraph); } // Scenario for (ScenarioWrapper scenario : feature.getScenarios()) { try { emitScenario(featureChap, scenario); } catch (DocumentException e) { log.warn("Failed to emit scenario '{}'", scenario.getName(), e); } } // try { document.add(featureChap); } catch (DocumentException e) { log.warn("Failed to emit feature '{}'", feature.getName(), e); } } private void emitScenario(Chapter featureChap, ScenarioWrapper scenario) throws DocumentException { Paragraph scenarioTitle = new Paragraph(scenario.getName(), configuration.scenarioTitleFont()); Section section = featureChap.addSection(scenarioTitle); Margin margin = configuration.getScenarioMargin(); section.setIndentationLeft(margin.marginLeft); if (configuration.shouldDisplayTags()) { emitScenarioTags(scenario, section); } emitScenarioDescription(scenario, section); emitScenarioEmbeddings(scenario, section); Paragraph steps = new Paragraph(""); steps.setKeepTogether(configuration.shouldKeepScenarioUnbreakable()); BackgroundWrapper background=scenario.getBackground(); for(StepWrapper backgroundStep : background.getSteps()){ emitStep(scenario, steps, backgroundStep); } for (StepWrapper scenarioStep : scenario.getSteps()) { emitStep(scenario, steps, scenarioStep); } steps.setSpacingBefore(margin.marginTop); steps.setSpacingAfter(margin.marginBottom); section.add(steps); } private void emitScenarioEmbeddings(ScenarioWrapper scenario, Section section) { for (Embedding embedding : scenario.getEmbeddings()) { section.addAll(toElements(embedding)); } } private void emitScenarioDescription(ScenarioWrapper scenario, Section section) throws DocumentException { String description = scenario.getDescription(); if (BricABrac.isBlank(description)) return; List<Element> elements = configuration.markdownContent(description); for (Element element : elements) { if (element instanceof PdfPTable) extendTableToWidth((PdfPTable) element, document.right() - document.left() - 25.0f); } Paragraph paragraph = new Paragraph(); paragraph.addAll(elements); Margin margin = configuration.getScenarioMargin(); paragraph.setIndentationLeft(margin.marginLeft); paragraph.setSpacingBefore(margin.marginTop); paragraph.setSpacingAfter(margin.marginBottom); section.add(paragraph); } private void emitScenarioTags(ScenarioWrapper scenario, Section section) { List<Tag> tagList = scenario.getTags(); if (!tagList.isEmpty()) { Paragraph tags = new Paragraph("Tags: ", configuration.defaultMetaFont()); boolean first = true; for (Tag tag : tagList) { String text = tag.getName(); if (first) { first = false; } else { text = ", " + tag.getName(); } tags.add(new Chunk(text, configuration.tagsFont())); } section.add(tags); } } private float documentContentWidth() { return document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin(); } private void emitStep(ScenarioWrapper scenario, Paragraph steps, StepWrapper step) { boolean isManualStep = scenario.hasTag(configuration.manualTag()); float imageWidth = 16.5f; float imageHeight = 10.5f; PdfPTable stepAsTable = new PdfPTable(2); stepAsTable.setWidthPercentage(100); stepAsTable.setKeepTogether(true); try { float imageCellWidth = imageWidth + 5f; stepAsTable.setTotalWidth(new float[]{imageCellWidth, documentContentWidth() - imageCellWidth}); } catch (DocumentException e) { log.warn("Step table issue", e); } Image stepStatus = isManualStep ? getManualStepImageOrNull() : getStepStatusAsImageOrNull(step); PdfPCell cell; if (stepStatus != null) { stepStatus.scaleAbsolute(imageWidth, imageHeight); cell = new PdfPCell(stepStatus); cell.setPaddingTop(2.0f); } else { cell = new PdfPCell(new Phrase("")); } cell.setBorder(Rectangle.NO_BORDER); stepAsTable.addCell(cell); Paragraph stepParagraph = new Paragraph(); stepParagraph.add(new Chunk(step.getKeyword(), configuration.stepKeywordFont())); if(!step.isMatching()) { stepParagraph.add(new Chunk(step.getName(), configuration.stepDefaultFont())); } else { for(StepWrapper.Tok tok : step.tokenizeBody()) { Font tokFont = configuration.stepDefaultFont(); if(tok.param) tokFont = configuration.stepParameterFont(); stepParagraph.add(new Chunk(tok.value, tokFont)); } } cell = new PdfPCell(stepParagraph); cell.setBorder(Rectangle.NO_BORDER); stepAsTable.addCell(cell); if (step.hasTable()) { // table added on stepParagraph is not visible... // thus it becomes a direct nested table PdfPTable table = createStepDataTable(step.getTableRows()); stepAsTable.addCell(noBorder(new PdfPCell(new Phrase("")))); stepAsTable.addCell(noBorder(new PdfPCell(table))); } if(step.hasDocString()) { DocString docString = step.getDocString(); Phrase phrase = new Phrase(docString.getValue(), configuration.stepDefaultFont()); stepAsTable.addCell(noBorder(new PdfPCell(new Phrase("")))); stepAsTable.addCell(noBorder(new PdfPCell(phrase))); } steps.add(stepAsTable); for (Embedding embedding : step.getEmbeddings()) { steps.addAll(toElements(embedding)); } } private List<? extends Element> toElements(Embedding embedding) { String mimeType = embedding.getMimeType(); if (mimeType.toLowerCase().startsWith("text/")) { String utf8 = embedding.getDataAsUTF8(); String markdown = "" + "[" + mimeType.substring("text/".length()) + "]\n" + "----\n" + utf8 + "\n" + "----"; return configuration.markdownContent(markdown); } return Arrays.asList(new Paragraph()); } private static PdfPCell noBorder(PdfPCell pdfPCell) { pdfPCell.setBorder(Rectangle.NO_BORDER); return pdfPCell; } private static PdfPCell sideBorder(PdfPCell pdfPCell) { return border(pdfPCell, Rectangle.LEFT + Rectangle.RIGHT); } private static PdfPCell border(PdfPCell pdfPCell, int side) { pdfPCell.setBorder(side); return pdfPCell; } private Image getStepStatusAsImageOrNull(StepWrapper step) { return getResourceImageOrNull(getStepStatusResourceName(step)); } private Image getManualStepImageOrNull() { return getResourceImageOrNull("images/manual.PNG"); } private Image getResourceImageOrNull(String resourceName) { try { ByteSource byteSource = asByteSource(getClass().getResource(resourceName)); byte[] bytes = byteSource.read(); return Image.getInstance(bytes); } catch (BadElementException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } private String getStepStatusResourceName(StepWrapper step) { if (step.isSuccess()) { return "images/ok-icon.PNG"; } else if (step.isPending()) { return "images/pending-icon.PNG"; } else if (step.isFailure()) { return "images/ko-icon.PNG"; } else if (step.isSkipped()) { return "images/skipped-icon.PNG"; } else { return "images/unknown-icon.PNG"; } } private Font getTableFont(boolean firstRow) { if (firstRow) { return configuration.stepDataTableHeaderFont(); } else { return configuration.stepDataTableContentFont(); } } private PdfPTable createStepDataTable(List<DataTableRow> tableRows) { PdfPTable table = createEmptyStepDataTable(tableRows); for (int j = 0; j < tableRows.size(); j++) { boolean firstRow = (j == 0); Row row = tableRows.get(j); List<String> cells = row.getCells(); Font font = getTableFont(firstRow); for (int i = 0; i < cells.size(); i++) { boolean firstColumn = (i == 0); String content = cells.get(i); PdfPCell c = new PdfPCell(new Phrase(content, font)); if (firstRow) { // c.setPaddingBottom(5); c.setHorizontalAlignment(Element.ALIGN_CENTER); BaseColor backgroundColor = configuration.stepDataTableHeaderBackground(); if (backgroundColor != null) { c.setBackgroundColor(backgroundColor); } } int border = 0; if (!firstColumn) { border += Rectangle.LEFT; } c.setBorder(border); table.addCell(c); } } return table; } private PdfPTable createEmptyStepDataTable(List<DataTableRow> tableRows) { PdfPTable table = new PdfPTable(getTableColumnCount(tableRows)); table.setTableEvent(new AlternatingBackgroundEvent(configuration.stepDataTableRowAlternateBackground())); try { int[] columnMaxSizes = getTableColumnsContentMaxLength(tableRows); table.setWidths(columnMaxSizes); } catch (DocumentException e) { // Should not append since columnMaxSizes comes from tableRows. } return table; } private int getTableColumnCount(List<DataTableRow> tableRows) { if (!tableRows.isEmpty()) { List<String> cells = tableRows.get(0).getCells(); if (cells != null) { return cells.size(); } } return 1; // weird } private int[] getTableColumnsContentMaxLength(List<DataTableRow> tableRows) { int[] columnMaxSizes = null; for (DataTableRow tableRow : tableRows) { List<String> cells = tableRow.getCells(); for (int i = 0; i < cells.size(); i++) { if (columnMaxSizes == null) { columnMaxSizes = new int[cells.size()]; } columnMaxSizes[i] = Math.max(columnMaxSizes[i], cells.get(i).length()); } } return columnMaxSizes; } public void done() { emitSummary(); closeDocumentAndFile(); // 2nd pass postProcessFile(); } private void closeDocumentAndFile() { try { if (document != null) { document.close(); } if (fileOutputStream != null) { fileOutputStream.close(); } log.info("Report generated {}", fileTmp.getAbsolutePath()); } catch (Exception e) { throw new FormatterException("Error while flushing report to disk", e); } } /** * 2nd pass post-processing */ private void postProcessFile() { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream(fileTmp); out = new FileOutputStream(fileDst); int startPage = lookupExtraInsertionPage() + 1; ColumnText ct = generateTableOfContent(); pageNumber.continueExtra(); PdfReader reader = new PdfReader(in); PdfStamper stamper = new PdfStamper(reader, out); while (true) { stamper.insertPage(++startPage, reader.getPageSize(1)); PdfContentByte under = stamper.getUnderContent(startPage); pageNumber.notifyPageChange(startPage); headerFooter.drawFooter(under, pageNumber.pageInfos()); PdfContentByte canvas = stamper.getOverContent(startPage); ct.setCanvas(canvas); ct.setSimpleColumn(36, 36, 559, 770); if (!ColumnText.hasMoreText(ct.go())) break; } stamper.close(); } catch (FileNotFoundException e) { log.error("Unable to reopen temporary generated file", e); } catch (DocumentException e) { log.error("Error during report post-processing", e); } catch (IOException e) { log.error("Error during report post-processing", e); } finally { IOUtils.closeQuietly(out); IOUtils.closeQuietly(in); } } private ColumnText generateTableOfContent() { ColumnText ct = new ColumnText(null); Chunk CONNECT = new Chunk(new LineSeparator(0.5f, 95, configuration.defaultColor(), Element.ALIGN_CENTER, -.5f)); Paragraph paragraph = new Paragraph(); paragraph.setSpacingBefore(20.0f); // first paragraph only ct.addElement(new Paragraph("Table of content", configuration.chapterTitleFont())); ct.addElement(new Paragraph("")); for (TableOfContents.Entry entry : tableOfContents.getEntries()) { if (entry.isExtra()) continue; Chunk chunk = new Chunk(entry.getText(), configuration.tocEntryFont()); paragraph.add(chunk); paragraph.add(CONNECT); paragraph.add(new Chunk("" + entry.getFormattedPageNumber(), configuration.tocEntryFont())); float indent = 10.0f * entry.getLevel(); paragraph.setIndentationLeft(indent); ct.addElement(paragraph); paragraph = new Paragraph(); } return ct; } private int lookupExtraInsertionPage() { int startPage = -1; for (PageInfos pageInfos : pageNumber.getEmittedPageInfos()) { if (!pageInfos.isExtra()) break; startPage = pageInfos.getRawPageNumber(); } return startPage; } private void emitSummary() { if (!configuration.shouldEmitSummary()) { return; } Chapter summary = configuration.createTitledChapter("Summary"); emitScenarioSummary(summary); emitStepsSummary(summary); emitTagsSummary(summary); try { document.add(summary); } catch (DocumentException e) { log.warn("Failed to add summary", e); } } private void emitTagsSummary(Chapter chapter) { PdfPTable table = new PdfPTable(new float[]{10.f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}); table.setHeaderRows(2); table.setTableEvent(new AlternatingBackgroundEvent(configuration.tableAlternateBackground())); PdfPCell cell = createTableHeaderCell("Tags"); cell.setRowspan(2); table.addCell(border(cell, Rectangle.RIGHT)); PdfPCell cellScenario = createTableHeaderCell("Scenario"); cellScenario.setColspan(3); table.addCell(sideBorder(cellScenario)); PdfPCell cellSteps = createTableHeaderCell("Steps"); cellSteps.setColspan(3); table.addCell(sideBorder(cellSteps)); // table.addCell(sideBorder(createTableHeaderCell("M."))); table.addCell(sideBorder(createTableHeaderCell("OK"))); table.addCell(sideBorder(createTableHeaderCell("KO"))); table.addCell(sideBorder(createTableHeaderCell("M."))); table.addCell(sideBorder(createTableHeaderCell("OK"))); table.addCell(sideBorder(createTableHeaderCell("KO"))); ColorThresholdSelector successColors = ColorThresholdSelectors.redOrangeGreenPercent(); ColorThresholdSelector errorColors = ColorThresholdSelectors.yellowOrangeRedPercent(); Map<String, Statistics> tagStatistics = statistics.getTagStatistics(); List<String> tags = Lists.newArrayList(tagStatistics.keySet()); Collections.sort(tags); for (String tag : tags) { Statistics tagStats = tagStatistics.get(tag); table.addCell(noBorder(new PdfPCell(new Phrase(tag)))); // table.addCell(sideBorder(addCell(errorColors, tagStats.getNbScenario(), tagStats.getNbScenarioManual()))); table.addCell(sideBorder(addCell(successColors, tagStats.getNbScenarioExceptManual(), tagStats.getNbScenarioSucceeded()))); table.addCell(sideBorder(addCell(errorColors, tagStats.getNbScenarioExceptManual(), tagStats.getNbScenarioExceptManual() - tagStats.getNbScenarioSucceeded()))); // table.addCell(sideBorder(addCell(errorColors, tagStats.getNbSteps(), tagStats.getNbStepManual()))); table.addCell(sideBorder(addCell(successColors, tagStats.getNbStepsExceptManual(), tagStats.getNbStepSucceeded()))); table.addCell(noBorder(addCell(errorColors, tagStats.getNbStepsExceptManual(), tagStats.getNbStepsExceptManual() - tagStats.getNbStepSucceeded()))); } Paragraph paragraph = new Paragraph(); paragraph.add(table); paragraph.setSpacingBefore(20f); paragraph.setSpacingAfter(20f); Section section = chapter.addSection(20f, new Paragraph("Tags", configuration.sectionTitleFont())); section.setIndentationLeft(20.0f); section.add(paragraph); } private PdfPCell createTableHeaderCell(String text) { PdfPCell pdfPCell = new PdfPCell(new Phrase(text, configuration.tableHeaderFont())); pdfPCell.setBackgroundColor(configuration.tableHeaderBackground()); pdfPCell.setVerticalAlignment(Element.ALIGN_MIDDLE); pdfPCell.setHorizontalAlignment(Element.ALIGN_CENTER); return pdfPCell; } private static PdfPCell addCell(ColorThresholdSelector colors, int total, int value) { PdfPCell cell = new PdfPCell(new Phrase("" + value)); cell.setCellEvent(new PercentBackgroundEvent(value, total, colors)); cell.setHorizontalAlignment(Element.ALIGN_RIGHT); return cell; } private void emitStepsSummary(Chapter chapter) { Paragraph paragraph = new Paragraph(); paragraph.add(createStepsManualStatisticsTable(statistics)); paragraph.add(createStepsAutomaticStatisticsTable(statistics)); paragraph.setSpacingAfter(20f); Section section = chapter.addSection(20f, new Paragraph("Steps", configuration.sectionTitleFont())); section.setIndentationLeft(20.0f); section.add(paragraph); } private void emitScenarioSummary(Chapter chapter) { Paragraph paragraph = new Paragraph(); paragraph.add(createScenarioManualStatisticsTable(statistics)); paragraph.add(createScenarioAutomaticStatisticsTable(statistics)); paragraph.setSpacingAfter(20f); Section section = chapter.addSection(20f, new Paragraph("Scenario", configuration.sectionTitleFont())); section.setIndentationLeft(20.0f); section.add(paragraph); } private Paragraph createScenarioManualStatisticsTable(Statistics statistics) { ColorThresholdSelector errorColors = ColorThresholdSelectors.yellowOrangeRedPercent(); PdfPTable table = new PdfPTable(2); int total = statistics.getNbScenario(); appendRow(table, errorColors, total, statistics.getNbScenarioManual(), "Manual"); Paragraph paragraph = new Paragraph(); Phrase phrase = new Phrase("Manual scenario", configuration.subSectionTitleFont()); paragraph.add(phrase); paragraph.add(table); paragraph.setSpacingBefore(20f); return paragraph; } private Paragraph createScenarioAutomaticStatisticsTable(Statistics statistics) { ColorThresholdSelector successColors = ColorThresholdSelectors.redOrangeGreenPercent(); ColorThresholdSelector errorColors = ColorThresholdSelectors.yellowOrangeRedPercent(); PdfPTable table = new PdfPTable(2); int total = statistics.getNbScenarioExceptManual(); appendRow(table, successColors, total, statistics.getNbScenarioSucceeded(), "Success"); appendRow(table, errorColors, total, statistics.getNbScenarioFailed(), "Failure"); appendRow(table, errorColors, total, statistics.getNbScenarioPending(), "Pending"); appendRow(table, errorColors, total, statistics.getNbScenarioSkipped(), "Skipped"); appendRow(table, errorColors, total, statistics.getNbScenarioOther(), "Other"); appendTotalRow(table, total, "Total"); Paragraph paragraph = new Paragraph(); Phrase phrase = new Phrase("Automatised scenario", configuration.subSectionTitleFont()); paragraph.add(phrase); paragraph.add(table); paragraph.setSpacingBefore(20f); return paragraph; } private Paragraph createStepsAutomaticStatisticsTable(Statistics statistics) { ColorThresholdSelector successColors = ColorThresholdSelectors.redOrangeGreenPercent(); ColorThresholdSelector errorColors = ColorThresholdSelectors.yellowOrangeRedPercent(); PdfPTable table = new PdfPTable(2); int total = statistics.getNbStepsExceptManual(); appendRow(table, successColors, total, statistics.getNbStepSucceeded(), "Success"); appendRow(table, errorColors, total, statistics.getNbStepFailed(), "Failure"); appendRow(table, errorColors, total, statistics.getNbStepPending(), "Pending"); appendRow(table, errorColors, total, statistics.getNbStepSkipped(), "Skipped"); appendRow(table, errorColors, total, statistics.getNbStepNoMatching(), "No matching"); appendRow(table, errorColors, total, statistics.getNbStepOther(), "Other"); appendTotalRow(table, total, "Total"); Paragraph paragraph = new Paragraph(); Phrase phrase = new Phrase("Automatised steps", configuration.subSectionTitleFont()); paragraph.add(phrase); paragraph.add(table); paragraph.setSpacingBefore(20f); return paragraph; } private Paragraph createStepsManualStatisticsTable(Statistics statistics) { ColorThresholdSelector errorColors = ColorThresholdSelectors.yellowOrangeRedPercent(); PdfPTable table = new PdfPTable(2); int total = statistics.getNbSteps(); appendRow(table, errorColors, total, statistics.getNbStepManual(), "Manual"); Paragraph paragraph = new Paragraph(); Phrase phrase = new Phrase("Manual steps", configuration.subSectionTitleFont()); paragraph.add(phrase); paragraph.add(table); paragraph.setSpacingBefore(20f); return paragraph; } private void appendTotalRow(PdfPTable table, int count, String title) { PdfPCell cellTitle = new PdfPCell(new Phrase(title, configuration.defaultStrongFont())); cellTitle.setHorizontalAlignment(Element.ALIGN_RIGHT); PdfPCell cellValue = new PdfPCell(new Phrase(String.valueOf(count))); cellValue.setHorizontalAlignment(Element.ALIGN_RIGHT); table.addCell(cellTitle); table.addCell(cellValue); } private void appendRow(PdfPTable table, ColorThresholdSelector colorSelector, int total, int count, String title) { PdfPCell cellTitle = new PdfPCell(new Phrase(title, configuration.defaultStrongFont())); PdfPCell cellValue = new PdfPCell(new Phrase(String.format("%d/%d", count, total))); cellValue.setHorizontalAlignment(Element.ALIGN_RIGHT); cellValue.setCellEvent(new PercentBackgroundEvent(count, total, colorSelector)); table.addCell(cellTitle); table.addCell(cellValue); } }