package tzatziki.pdf.emitter; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.itextpdf.text.BaseColor; import com.itextpdf.text.Element; import com.itextpdf.text.Font; import com.itextpdf.text.Paragraph; import com.itextpdf.text.Phrase; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.PdfPCell; import com.itextpdf.text.pdf.PdfPTable; import gutenberg.itext.Colors; import gutenberg.itext.Emitter; import gutenberg.itext.FontCopier; import gutenberg.itext.ITextContext; import gutenberg.itext.Sections; import gutenberg.itext.Styles; import gutenberg.itext.model.Markdown; import gutenberg.util.KeyValues; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tzatziki.analysis.exec.model.ExamplesExec; import tzatziki.analysis.exec.model.ExamplesRow; import tzatziki.analysis.exec.model.ScenarioExec; import tzatziki.analysis.exec.model.ScenarioOutlineExec; import tzatziki.analysis.exec.model.Status; import tzatziki.analysis.exec.model.StepExec; import tzatziki.pdf.Comments; import tzatziki.pdf.Settings; import tzatziki.pdf.model.ScenarioOutlineWithResolved; import tzatziki.pdf.model.Steps; import tzatziki.pdf.model.Tags; import java.util.Iterator; import java.util.List; import static com.google.common.base.Predicates.not; /** * @author <a href="http://twitter.com/aloyer">@aloyer</a> */ public class ScenarioOutlineEmitter implements Emitter<ScenarioOutlineWithResolved> { public static final String DISPLAY_TAGS = "scenario-display-tags"; public static final String EXAMPLES_CELL_HEADER = "examples-table-cell-header"; public static final String EXAMPLES_CELL = "examples-table-cell"; private boolean debugTable = false; private StatusMarker statusMarker = new StatusMarker(); private final int hLevel; private StepContainerEmitter stepsEmitter; private Logger log = LoggerFactory.getLogger(FeatureEmitter.class); public ScenarioOutlineEmitter() { this(2); } public ScenarioOutlineEmitter(int hLevel) { this(hLevel, new StepContainerEmitter()); } public ScenarioOutlineEmitter(int hLevel, StepContainerEmitter stepsEmitter) { this.hLevel = hLevel; this.stepsEmitter = stepsEmitter; } @Override public void emit(ScenarioOutlineWithResolved scenarioOutlineWithResolved, final ITextContext emitterContext) { Sections sections = emitterContext.sections(); KeyValues kvs = emitterContext.keyValues(); Integer rawOffset = kvs.getInteger(FeatureEmitter.FEATURE_HEADER_LEVEL_OFFSET).or(0); final int headerLevel = hLevel + rawOffset; ScenarioOutlineExec outline = scenarioOutlineWithResolved.outline(); sections.newSection(outline.name(), headerLevel); try { if (kvs.getBoolean(DISPLAY_TAGS, true)) { stepsEmitter.emitTags(outline, emitterContext); } stepsEmitter.emitDescription(outline, emitterContext); stepsEmitter.emitEmbeddings(outline, emitterContext); stepsEmitter.emitSteps(outline, emitterContext); emitExamples(scenarioOutlineWithResolved, emitterContext, headerLevel, outline); } finally { sections.leaveSection(headerLevel); // end-of-scenario-outline } } private void emitExamples(ScenarioOutlineWithResolved scenarioOutlineWithResolved, final ITextContext emitterContext, final int headerLevel, ScenarioOutlineExec outline) { final Iterator<ScenarioExec> scenarioIt = scenarioOutlineWithResolved.resolved().iterator(); // due to the iterator usage, one must not rely on iterable afterwards // otherwise scenarioIt will be reconsumed everytime an iteration is performed through the iterable List<ExamplesEmitResult> results = outline.examples().transform(new Function<ExamplesExec, ExamplesEmitResult>() { @Override public ExamplesEmitResult apply(ExamplesExec input) { return generateExamples(input, scenarioIt, emitterContext, headerLevel + 1); } }).toList(); boolean shouldEmitStepInError = FluentIterable.from(results).firstMatch(new Predicate<ExamplesEmitResult>() { @Override public boolean apply(ExamplesEmitResult input) { return input.stepInError; } }).isPresent(); if (shouldEmitStepInError) { emitStepInErrorLegend(emitterContext); } for (ExamplesEmitResult result : results) { emitterContext.append(result.exampleTable); } } private void emitStepInErrorLegend(ITextContext emitterContext) { Styles styles = emitterContext.styles(); PdfPTable table = new PdfPTable(new float[]{1f, 24f}); table.addCell(noBorder(topRight(new PdfPCell(stepInErrorMarker(styles))))); table.addCell(noBorder(new PdfPCell(new Phrase(": First step in error within the scenario", new FontCopier(styles.defaultFont()).italic().get())))); table.setSpacingBefore(10f); table.setSpacingAfter(10f); emitterContext.append(table); } private ExamplesEmitResult generateExamples(ExamplesExec examplesExec, Iterator<ScenarioExec> scenarioIt, ITextContext emitterContext, int headerLevel) { ExamplesEmitResult result = new ExamplesEmitResult(); KeyValues kvs = emitterContext.keyValues(); Styles styles = kvs.<Styles>getNullable(Styles.class).get(); Sections sections = emitterContext.sections(); sections.newSection(firstNotNullOrEmpty(examplesExec.name(), examplesExec.keyword()), headerLevel); try { float[] widths = computeWidths(examplesExec); PdfPTable table = new PdfPTable(widths); table.setWidthPercentage(100); int nbCols = examplesExec.columnCount(); int rownum = 0; BaseColor alternateBG = styles.getColor(Styles.TABLE_ALTERNATE_BACKGROUND).or(Colors.VERY_LIGHT_GRAY); boolean alternate = true; for (ExamplesRow row : examplesExec.rows()) { alternate = !alternate; BaseColor background = alternate ? alternateBG : null; Font font = getExamplesRowFont(styles, rownum); Optional<StepExec> firstStepInError = Optional.absent(); if (rownum == 0) { background = styles.getColor(Styles.TABLE_HEADER_BACKGROUD).get(); PdfPCell status = new PdfPCell(new Phrase("")); table.addCell(withBackground(noBorder(status), background)); } else { ScenarioExec scenarioExec = scenarioIt.next(); Status status = scenarioExec.status(); table.addCell(withBackground(statusCell(status), background)); if (status != Status.Passed) { firstStepInError = scenarioExec.steps().firstMatch(not(StepExec.statusPassed)); } } for (String value : row.cells()) { PdfPCell cell = new PdfPCell(new Phrase(value, font)); if (rownum > 0) { cell.setBorder(Rectangle.TOP); cell.setBorderColor(BaseColor.LIGHT_GRAY); } else { cell.setBorder(Rectangle.NO_BORDER); } table.addCell(withBackground(cell, background)); } if (firstStepInError.isPresent()) { result.stepInError = true; PdfPCell cell = new PdfPCell(stepInErrorMarker(styles)); table.addCell(withBackground(noBorder(topRight(cell)), background)); table.addCell(withBackground(colspan(nbCols, stepCell(firstStepInError.get(), styles, emitterContext)), background)); } rownum++; } result.exampleTable = table; } catch (Exception e) { log.warn("Fail to emit outline examples", e); throw new RuntimeException(e); } finally { sections.leaveSection(headerLevel); // end-of-examples } return result; } private PdfPCell withBackground(PdfPCell cell, BaseColor background) { if (background != null) cell.setBackgroundColor(background); return cell; } private float[] computeWidths(ExamplesExec examples) { int nbCols = examples.columnCount(); final int[] maxWidth = new int[nbCols]; for (ExamplesRow examplesRow : examples.rows()) { int c = 0; for (String s : examplesRow.cells()) { maxWidth[c] = Math.max(s.length(), maxWidth[c]); c++; } } float sum = 0; for (int i : maxWidth) { sum += i; } float[] widths = new float[1 + nbCols]; widths[0] = 1f; for (int i = 1; i <= nbCols; i++) { widths[i] = 24f * maxWidth[i - 1] / sum; } return widths; } private Font getExamplesRowFont(Styles styles, int rownum) { Font font = null; if (rownum == 0) { Optional<Font> fontOpt = styles.getFont(EXAMPLES_CELL_HEADER); if (fontOpt.isPresent()) { font = fontOpt.get(); } else { font = styles.getFont(Styles.TABLE_HEADER_FONT).get(); } } if (font == null) font = styles.getFontOrDefault(EXAMPLES_CELL); return font; } private Phrase stepInErrorMarker(Styles styles) { return new Phrase(stepInErrorMarker(), styles.getFontOrDefault(Settings.META_FONT)); } private String stepInErrorMarker() { return "*"; } private PdfPCell topRight(PdfPCell pdfPCell) { pdfPCell.setVerticalAlignment(Element.ALIGN_TOP); pdfPCell.setHorizontalAlignment(Element.ALIGN_RIGHT); return pdfPCell; } private PdfPCell stepCell(StepExec stepExec, Styles styles, ITextContext context) { Paragraph stepPhrase = StepsEmitter.formatStep(stepExec, true, styles, context); PdfPCell stepCell = new PdfPCell(stepPhrase); stepCell.setVerticalAlignment(Element.ALIGN_MIDDLE); stepCell.setHorizontalAlignment(Element.ALIGN_LEFT); stepCell = noBorder(stepCell); return stepCell; } private static String firstNotNullOrEmpty(String one, String two) { if (one != null && !one.trim().isEmpty()) return one; return two; } private PdfPCell statusCell(Status status) { Phrase statusSymbol = new Phrase(statusMarker.statusMarker(status)); PdfPCell statusCell = new PdfPCell(statusSymbol); statusCell.setVerticalAlignment(Element.ALIGN_TOP); statusCell.setHorizontalAlignment(Element.ALIGN_RIGHT); statusCell = noBorder(statusCell); return statusCell; } private static PdfPCell colspan(int colspan, PdfPCell cell) { cell.setColspan(colspan); return cell; } private PdfPCell noBorder(PdfPCell cell) { if (!debugTable) { cell.setBorder(Rectangle.NO_BORDER); } return cell; } private static class ExamplesEmitResult { public boolean stepInError = false; public PdfPTable exampleTable; } }