All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.touk.nussknacker.ui.util.PdfExporter.scala Maven / Gradle / Ivy

There is a newer version: 1.17.0
Show newest version
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} ) }
} }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy