Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
pl.touk.nussknacker.ui.util.PdfExporter.scala Maven / Gradle / Ivy
package pl.touk.nussknacker.ui.util
import java.io._
import java.net.URI
import java.nio.charset.StandardCharsets
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.sax.SAXResult
import javax.xml.transform.stream.StreamSource
import com.typesafe.scalalogging.LazyLogging
import org.apache.commons.io.IOUtils
import org.apache.fop.apps.FopConfParser
import org.apache.fop.apps.io.ResourceResolverFactory
import org.apache.xmlgraphics.util.MimeConstants
import pl.touk.nussknacker.engine.api.graph.ScenarioGraph
import pl.touk.nussknacker.engine.graph.node._
import pl.touk.nussknacker.engine.graph.service.ServiceRef
import pl.touk.nussknacker.engine.graph.sink.SinkRef
import pl.touk.nussknacker.engine.graph.source.SourceRef
import pl.touk.nussknacker.engine.graph.fragment.FragmentRef
import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.ProcessActivity
import pl.touk.nussknacker.ui.process.repository.ScenarioWithDetailsEntity
import scala.xml.{Elem, NodeSeq, XML}
object PdfExporter extends LazyLogging {
private val fopFactory = new FopConfParser(
getClass.getResourceAsStream("/fop/config.xml"),
new URI("http://touk.pl"),
ResourceResolverFactory.createDefaultResourceResolver
).getFopFactoryBuilder.build
def exportToPdf(
svg: String,
processDetails: ScenarioWithDetailsEntity[ScenarioGraph],
processActivity: ProcessActivity
): Array[Byte] = {
// initFontsIfNeeded is invoked every time to make sure that /tmp content is not deleted
initFontsIfNeeded()
// FIXME: cannot render polish signs..., better to strip them than not render anything...
// \u00A0 - non-breaking space in not ASCII :)...
val fopXml = prepareFopXml(
svg.replaceAll("\u00A0", " ").replaceAll("[^\\p{ASCII}]", ""),
processDetails,
processActivity,
processDetails.json
)
createPdf(fopXml)
}
// in PDF export we print timezone, to avoid ambiguity
// TODO: pass client timezone from FE
private def format(instant: Instant) = {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss [VV]")
instant.atZone(ZoneId.systemDefault()).format(formatter)
}
// TODO: this is one nasty hack, is there a better way to make fop read fonts from classpath?
private def initFontsIfNeeded(): Unit = synchronized {
val dir = new File("/tmp/fop/fonts")
dir.mkdirs()
List(
"OpenSans-BoldItalic.ttf",
"OpenSans-Bold.ttf",
"OpenSans-ExtraBoldItalic.ttf",
"OpenSans-ExtraBold.ttf",
"OpenSans-Italic.ttf",
"OpenSans-LightItalic.ttf",
"OpenSans-Light.ttf",
"OpenSans-Regular.ttf",
"OpenSans-SemiboldItalic.ttf",
"OpenSans-Semibold.ttf"
).filterNot(name => new File(dir, name).exists()).foreach { name =>
IOUtils.copy(getClass.getResourceAsStream(s"/fop/fonts/$name"), new FileOutputStream(new File(dir, name)))
}
}
private def createPdf(fopXml: Elem): Array[Byte] = {
val out = new ByteArrayOutputStream()
val fop = fopFactory.newFop(MimeConstants.MIME_PDF, out)
val src = new StreamSource(new ByteArrayInputStream(fopXml.toString().getBytes(StandardCharsets.UTF_8)))
TransformerFactory.newInstance().newTransformer().transform(src, new SAXResult(fop.getDefaultHandler))
out.toByteArray
}
private def prepareFopXml(
svg: String,
processDetails: ScenarioWithDetailsEntity[ScenarioGraph],
processActivity: ProcessActivity,
scenarioGraph: ScenarioGraph
) = {
val diagram = XML.loadString(svg)
val currentVersion = processDetails.history.get.find(_.processVersionId == processDetails.processVersionId).get
{processDetails.name}
(
{processDetails.processCategory}
)
Version:
{processDetails.processVersionId}
Saved by
{currentVersion.user}
at
{format(currentVersion.createDate)}
{processDetails.description.getOrElse("")}
{diagram}
{nodesSummary(scenarioGraph)}
Nodes details
{scenarioGraph.nodes.map(nodeDetails)}{comments(processActivity)}{attachments(processActivity)}
}
private def comments(processActivity: ProcessActivity) =
Comments
Date
Author
Comment
{
if (processActivity.comments.isEmpty) {
} else
processActivity.comments.sortBy(_.createDate).map { comment =>
{format(comment.createDate)}
{comment.user}
{comment.content}
}
}
private def nodeDetails(node: NodeData) = {
val nodeData = node match {
case Source(_, SourceRef(typ, params), _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression))
case Filter(_, expression, _, _) => List(("Expression", expression.expression))
case Enricher(_, ServiceRef(typ, params), output, _) =>
("Type", typ) :: ("Output", output) :: params.map(p => (p.name, p.expression.expression))
// TODO: what about Swtich??
case Switch(_, expression, exprVal, _) => expression.map(e => ("Expression", e.expression)).toList
case Processor(_, ServiceRef(typ, params), _, _) =>
("Type", typ) :: params.map(p => (p.name, p.expression.expression))
case Sink(_, SinkRef(typ, params), _, _, _) => ("Type", typ) :: params.map(p => (p.name, p.expression.expression))
case CustomNode(_, output, typ, params, _) =>
("Type", typ) :: ("Output", output.getOrElse("")) :: params.map(p => (p.name, p.expression.expression))
case FragmentInput(_, FragmentRef(typ, params, _), _, _, _) =>
("Type", typ) :: params.map(p => (p.name, p.expression.expression))
case FragmentInputDefinition(_, parameters, _) => parameters.map(p => p.name -> p.typ.refClazzName)
case FragmentOutputDefinition(_, outputName, fields, _) =>
("Output name", outputName) :: fields.map(p => p.name -> p.expression.expression)
case Variable(_, name, expr, _) => (name -> expr.expression) :: Nil
case VariableBuilder(_, name, fields, _) =>
("Variable name", name) :: fields.map(p => p.name -> p.expression.expression)
case Join(_, output, typ, parameters, branch, _) =>
("Type", typ) :: ("Output", output.getOrElse("")) ::
parameters.map(p => p.name -> p.expression.expression) ++ branch.flatMap(bp =>
bp.parameters.map(p => s"${bp.branchId} - ${p.name}" -> p.expression.expression)
)
case Split(_, _) => ("No parameters", "") :: Nil
// This should not happen in properly resolved scenario...
case _: BranchEndData => throw new IllegalArgumentException("Should not happen during PDF export")
case _: FragmentUsageOutput => throw new IllegalArgumentException("Should not happen during PDF export")
}
val data = node.additionalFields
.flatMap(_.description)
.map(naf => ("Description", naf))
.toList ++ nodeData
if (data.isEmpty) {
NodeSeq.Empty
} else {
{node.getClass.getSimpleName}{node.id}
{
data.map { case (key, value) =>
{key}
{addEmptySpace(value)}
}
}
}
}
// we want to be able to break line for these characters. it's not really perfect solution for long, complex expressions,
// but should handle most of the cases../
private def addEmptySpace(str: String) = List(")", ".", "(")
.foldLeft(str) { (acc, el) => acc.replace(el, el + '\u200b') }
private def nodesSummary(scenarioGraph: ScenarioGraph) = {
Nodes summary
Node name
Type
Description
{
if (scenarioGraph.nodes.isEmpty) {
} else
scenarioGraph.nodes.map { node =>
{node.id}
{node.getClass.getSimpleName}
{node.additionalFields.flatMap(_.description).getOrElse("")}
}
}
}
private def attachments(processActivity: ProcessActivity) = if (processActivity.attachments.isEmpty) {
} else {
Attachments
Date
Author
File name
{
processActivity.attachments
.sortBy(_.createDate)
.map(attachment =>
{format(attachment.createDate)}
{attachment.user}
{attachment.fileName}
)
}
}
}