diff options
author | Paul Phillips <paulp@improving.org> | 2013-03-06 07:39:19 -0800 |
---|---|---|
committer | Adriaan Moors <adriaan.moors@typesafe.com> | 2013-03-09 11:59:07 -0800 |
commit | c6ca941ccc017a8869f4def717cfeb640f965077 (patch) | |
tree | e21038cc55d3a4231544d148913504a754eafdf8 /src/scaladoc/scala/tools/nsc/doc/html/page/diagram | |
parent | 9094822181c398b945b7f30ac1e2b05da9796f53 (diff) | |
download | scala-c6ca941ccc017a8869f4def717cfeb640f965077.tar.gz scala-c6ca941ccc017a8869f4def717cfeb640f965077.tar.bz2 scala-c6ca941ccc017a8869f4def717cfeb640f965077.zip |
Moved scaladoc sources into separate directory.
This change is not externally visible. It moves the scaladoc
sources into src/scaladoc and adds an ant target for building
them. The compilation products are still packaged into
scala-compiler.jar as before, but with a small change to
build.xml a separate jar can be created instead.
Diffstat (limited to 'src/scaladoc/scala/tools/nsc/doc/html/page/diagram')
4 files changed, 850 insertions, 0 deletions
diff --git a/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala new file mode 100644 index 0000000000..61c1819d11 --- /dev/null +++ b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramGenerator.scala @@ -0,0 +1,53 @@ +/** + * @author Damien Obrist + * @author Vlad Ureche + */ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import scala.xml.NodeSeq +import scala.tools.nsc.doc.html.HtmlPage +import scala.tools.nsc.doc.model.diagram.Diagram +import scala.tools.nsc.doc.model.DocTemplateEntity + +trait DiagramGenerator { + + /** + * Generates a visualization of the internal representation + * of a diagram. + * + * @param d The model of the diagram + * @param p The page the diagram will be embedded in (needed for link generation) + * @return The HTML to be embedded in the Scaladoc page + */ + def generate(d: Diagram, t: DocTemplateEntity, p: HtmlPage):NodeSeq +} + +object DiagramGenerator { + + // TODO: This is tailored towards the dot generator, since it's the only generator. In the future it should be more + // general. + + private[this] var dotRunner: DotRunner = null + private[this] var settings: doc.Settings = null + + def initialize(s: doc.Settings) = + settings = s + + def getDotRunner() = { + if (dotRunner == null) + dotRunner = new DotRunner(settings) + dotRunner + } + + def cleanup() = { + DiagramStats.printStats(settings) + if (dotRunner != null) { + dotRunner.cleanup() + dotRunner = null + } + } +}
\ No newline at end of file diff --git a/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala new file mode 100644 index 0000000000..ec00cace75 --- /dev/null +++ b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DiagramStats.scala @@ -0,0 +1,66 @@ +/** + * @author Vlad Ureche + */ +package scala.tools.nsc.doc +package html.page.diagram + +object DiagramStats { + + class TimeTracker(title: String) { + var totalTime: Long = 0l + var maxTime: Long = 0l + var instances: Int = 0 + + def addTime(ms: Long) = { + if (maxTime < ms) + maxTime = ms + totalTime += ms + instances += 1 + } + + def printStats(print: String => Unit) = { + if (instances == 0) + print(title + ": no stats gathered") + else { + print(" " + title) + print(" " + "=" * title.length) + print(" count: " + instances + " items") + print(" total time: " + totalTime + " ms") + print(" average time: " + (totalTime/instances) + " ms") + print(" maximum time: " + maxTime + " ms") + print("") + } + } + } + + private[this] val filterTrack = new TimeTracker("diagrams model filtering") + private[this] val modelTrack = new TimeTracker("diagrams model generation") + private[this] val dotGenTrack = new TimeTracker("dot diagram generation") + private[this] val dotRunTrack = new TimeTracker("dot process runnning") + private[this] val svgTrack = new TimeTracker("svg processing") + private[this] var brokenImages = 0 + private[this] var fixedImages = 0 + + def printStats(settings: Settings) = { + if (settings.docDiagramsDebug.value) { + settings.printMsg("\nDiagram generation running time breakdown:\n") + filterTrack.printStats(settings.printMsg) + modelTrack.printStats(settings.printMsg) + dotGenTrack.printStats(settings.printMsg) + dotRunTrack.printStats(settings.printMsg) + svgTrack.printStats(settings.printMsg) + println(" Broken images: " + brokenImages) + println(" Fixed images: " + fixedImages) + println("") + } + } + + def addFilterTime(ms: Long) = filterTrack.addTime(ms) + def addModelTime(ms: Long) = modelTrack.addTime(ms) + def addDotGenerationTime(ms: Long) = dotGenTrack.addTime(ms) + def addDotRunningTime(ms: Long) = dotRunTrack.addTime(ms) + def addSvgTime(ms: Long) = svgTrack.addTime(ms) + + def addBrokenImage(): Unit = brokenImages += 1 + def addFixedImage(): Unit = fixedImages += 1 +}
\ No newline at end of file diff --git a/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala new file mode 100644 index 0000000000..837d9e6f21 --- /dev/null +++ b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotDiagramGenerator.scala @@ -0,0 +1,506 @@ +/** + * @author Damien Obrist + * @author Vlad Ureche + */ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import scala.xml.{NodeSeq, XML, PrefixedAttribute, Elem, MetaData, Null, UnprefixedAttribute} +import scala.collection.immutable._ +import model._ +import model.diagram._ + +class DotDiagramGenerator(settings: doc.Settings) extends DiagramGenerator { + + // the page where the diagram will be embedded + private var page: HtmlPage = null + // path to the "lib" folder relative to the page + private var pathToLib: String = null + // maps nodes to unique indices + private var node2Index: Map[Node, Int] = null + // true if the current diagram is a class diagram + private var isInheritanceDiagram = false + // incoming implicit nodes (needed for determining the CSS class of a node) + private var incomingImplicitNodes: List[Node] = List() + // the suffix used when there are two many classes to show + private final val MultiSuffix = " classes/traits" + // used to generate unique node and edge ids (i.e. avoid conflicts with multiple diagrams) + private var counter = 0 + + def generate(diagram: Diagram, template: DocTemplateEntity, page: HtmlPage):NodeSeq = { + counter = counter + 1 + this.page = page + pathToLib = "../" * (page.templateToPath(template).size - 1) + "lib/" + val dot = generateDot(diagram) + val result = generateSVG(dot, template) + // clean things up a bit, so we don't leave garbage on the heap + this.page = null + node2Index = null + incomingImplicitNodes = List() + result + } + + /** + * Generates a dot string for a given diagram. + */ + private def generateDot(d: Diagram) = { + // inheritance nodes (all nodes except thisNode and implicit nodes) + var nodes: List[Node] = null + // inheritance edges (all edges except implicit edges) + var edges: List[(Node, List[Node])] = null + + // timing + var tDot = -System.currentTimeMillis + + // variables specific to class diagrams: + // current node of a class diagram + var thisNode:Node = null + var subClasses = List[Node]() + var superClasses = List[Node]() + var incomingImplicits = List[Node]() + var outgoingImplicits = List[Node]() + isInheritanceDiagram = false + + d match { + case InheritanceDiagram(_thisNode, _superClasses, _subClasses, _incomingImplicits, _outgoingImplicits) => + + def textTypeEntity(text: String) = + new TypeEntity { + val name = text + def refEntity: SortedMap[Int, (base.LinkTo, Int)] = SortedMap() + } + + // it seems dot chokes on node names over 8000 chars, so let's limit the size of the string + // conservatively, we'll limit at 4000, to be sure: + def limitSize(str: String) = if (str.length > 4000) str.substring(0, 3996) + " ..." else str + + // avoid overcrowding the diagram: + // if there are too many super / sub / implicit nodes, represent + // them by on node with a corresponding tooltip + superClasses = if (_superClasses.length > settings.docDiagramsMaxNormalClasses.value) { + val superClassesTooltip = Some(limitSize(_superClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_superClasses.length + MultiSuffix), None)(superClassesTooltip)) + } else _superClasses + + subClasses = if (_subClasses.length > settings.docDiagramsMaxNormalClasses.value) { + val subClassesTooltip = Some(limitSize(_subClasses.map(_.tpe.name).mkString(", "))) + List(NormalNode(textTypeEntity(_subClasses.length + MultiSuffix), None)(subClassesTooltip)) + } else _subClasses + + incomingImplicits = if (_incomingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { + val incomingImplicitsTooltip = Some(limitSize(_incomingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_incomingImplicits.length + MultiSuffix), None)(incomingImplicitsTooltip)) + } else _incomingImplicits + + outgoingImplicits = if (_outgoingImplicits.length > settings.docDiagramsMaxImplicitClasses.value) { + val outgoingImplicitsTooltip = Some(limitSize(_outgoingImplicits.map(_.tpe.name).mkString(", "))) + List(ImplicitNode(textTypeEntity(_outgoingImplicits.length + MultiSuffix), None)(outgoingImplicitsTooltip)) + } else _outgoingImplicits + + thisNode = _thisNode + nodes = List() + edges = (thisNode -> superClasses) :: subClasses.map(_ -> List(thisNode)) + node2Index = (thisNode::subClasses:::superClasses:::incomingImplicits:::outgoingImplicits).zipWithIndex.toMap + isInheritanceDiagram = true + incomingImplicitNodes = incomingImplicits + case _ => + nodes = d.nodes + edges = d.edges + node2Index = d.nodes.zipWithIndex.toMap + incomingImplicitNodes = List() + } + + val implicitsDot = { + if (!isInheritanceDiagram) "" + else { + // dot cluster containing thisNode + val thisCluster = "subgraph clusterThis {\n" + + "style=\"invis\"\n" + + node2Dot(thisNode) + + "}" + // dot cluster containing incoming implicit nodes, if any + val incomingCluster = { + if(incomingImplicits.isEmpty) "" + else "subgraph clusterIncoming {\n" + + "style=\"invis\"\n" + + incomingImplicits.reverse.map(n => node2Dot(n)).mkString + + (if (incomingImplicits.size > 1) + incomingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" + else "") + + "}" + } + // dot cluster containing outgoing implicit nodes, if any + val outgoingCluster = { + if(outgoingImplicits.isEmpty) "" + else "subgraph clusterOutgoing {\n" + + "style=\"invis\"\n" + + outgoingImplicits.reverse.map(n => node2Dot(n)).mkString + + (if (outgoingImplicits.size > 1) + outgoingImplicits.map(n => "node" + node2Index(n)).mkString(" -> ") + + " [constraint=\"false\", style=\"invis\", minlen=\"0.0\"];\n" + else "") + + "}" + } + + // assemble clusters into another cluster + val incomingTooltip = incomingImplicits.map(_.name).mkString(", ") + " can be implicitly converted to " + thisNode.name + val outgoingTooltip = thisNode.name + " can be implicitly converted to " + outgoingImplicits.map(_.name).mkString(", ") + "subgraph clusterAll {\n" + + "style=\"invis\"\n" + + outgoingCluster + "\n" + + thisCluster + "\n" + + incomingCluster + "\n" + + // incoming implicit edge + (if (!incomingImplicits.isEmpty) { + val n = incomingImplicits.last + "node" + node2Index(n) +" -> node" + node2Index(thisNode) + + " [id=\"" + cssClass(n, thisNode) + "|" + node2Index(n) + "_" + node2Index(thisNode) + "\", tooltip=\"" + incomingTooltip + "\"" + + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterIncoming\", lhead=\"clusterThis\", label=\"implicitly\"];\n" + } else "") + + // outgoing implicit edge + (if (!outgoingImplicits.isEmpty) { + val n = outgoingImplicits.head + "node" + node2Index(thisNode) + " -> node" + node2Index(n) + + " [id=\"" + cssClass(thisNode, n) + "|" + node2Index(thisNode) + "_" + node2Index(n) + "\", tooltip=\"" + outgoingTooltip + "\"" + + ", constraint=\"false\", minlen=\"2\", ltail=\"clusterThis\", lhead=\"clusterOutgoing\", label=\"implicitly\"];\n" + } else "") + + "}" + } + } + + // assemble graph + val graph = "digraph G {\n" + + // graph / node / edge attributes + graphAttributesStr + + "node [" + nodeAttributesStr + "];\n" + + "edge [" + edgeAttributesStr + "];\n" + + implicitsDot + "\n" + + // inheritance nodes + nodes.map(n => node2Dot(n)).mkString + + subClasses.map(n => node2Dot(n)).mkString + + superClasses.map(n => node2Dot(n)).mkString + + // inheritance edges + edges.map{ case (from, tos) => tos.map(to => { + val id = "graph" + counter + "_" + node2Index(to) + "_" + node2Index(from) + // the X -> Y edge is inverted twice to keep the diagram flowing the right way + // that is, an edge from node X to Y will result in a dot instruction nodeY -> nodeX [dir="back"] + "node" + node2Index(to) + " -> node" + node2Index(from) + + " [id=\"" + cssClass(to, from) + "|" + id + "\", " + + "tooltip=\"" + from.name + (if (from.name.endsWith(MultiSuffix)) " are subtypes of " else " is a subtype of ") + + to.name + "\", dir=\"back\", arrowtail=\"empty\"];\n" + }).mkString}.mkString + + "}" + + tDot += System.currentTimeMillis + DiagramStats.addDotGenerationTime(tDot) + + graph + } + + /** + * Generates the dot string of a given node. + */ + private def node2Dot(node: Node) = { + + // escape HTML characters in node names + def escape(name: String) = name.replace("&", "&").replace("<", "<").replace(">", ">") + + // assemble node attribues in a map + val attr = scala.collection.mutable.Map[String, String]() + + // link + node.doctpl match { + case Some(tpl) => attr += "URL" -> (page.relativeLinkTo(tpl) + "#inheritance-diagram") + case _ => + } + + // tooltip + node.tooltip match { + case Some(text) => attr += "tooltip" -> text + // show full name where available (instead of TraversableOps[A] show scala.collection.parallel.TraversableOps[A]) + case None if node.tpl.isDefined => attr += "tooltip" -> node.tpl.get.qualifiedName + case _ => + } + + // styles + if(node.isImplicitNode) + attr ++= implicitStyle + else if(node.isOutsideNode) + attr ++= outsideStyle + else if(node.isTraitNode) + attr ++= traitStyle + else if(node.isClassNode) + attr ++= classStyle + else if(node.isObjectNode) + attr ++= objectStyle + else if(node.isTypeNode) + attr ++= typeStyle + else + attr ++= defaultStyle + + // HTML label + var name = escape(node.name) + var img = "" + if(node.isTraitNode) + img = "trait_diagram.png" + else if(node.isClassNode) + img = "class_diagram.png" + else if(node.isObjectNode) + img = "object_diagram.png" + else if(node.isTypeNode) + img = "type_diagram.png" + + if(!img.equals("")) { + img = "<TD><IMG SCALE=\"TRUE\" SRC=\"" + settings.outdir.value + "/lib/" + img + "\" /></TD>" + name = name + " " + } + val label = "<<TABLE BORDER=\"0\" CELLBORDER=\"0\">" + + "<TR>" + img + "<TD VALIGN=\"MIDDLE\">" + name + "</TD></TR>" + + "</TABLE>>" + + // dot does not allow to specify a CSS class, therefore + // set the id to "{class}|{id}", which will be used in + // the transform method + val id = "graph" + counter + "_" + node2Index(node) + attr += ("id" -> (cssClass(node) + "|" + id)) + + // return dot string + "node" + node2Index(node) + " [label=" + label + "," + flatten(attr.toMap) + "];\n" + } + + /** + * Returns the CSS class for an edge connecting node1 and node2. + */ + private def cssClass(node1: Node, node2: Node): String = { + if (node1.isImplicitNode && node2.isThisNode) + "implicit-incoming" + else if (node1.isThisNode && node2.isImplicitNode) + "implicit-outgoing" + else + "inheritance" + } + + /** + * Returns the CSS class for a node. + */ + private def cssClass(node: Node): String = + if (node.isImplicitNode && incomingImplicitNodes.contains(node)) + "implicit-incoming" + cssBaseClass(node, "", " ") + else if (node.isImplicitNode) + "implicit-outgoing" + cssBaseClass(node, "", " ") + else if (node.isThisNode) + "this" + cssBaseClass(node, "", " ") + else if (node.isOutsideNode) + "outside" + cssBaseClass(node, "", " ") + else + cssBaseClass(node, "default", "") + + private def cssBaseClass(node: Node, default: String, space: String) = + if (node.isClassNode) + space + "class" + else if (node.isTraitNode) + space + "trait" + else if (node.isObjectNode) + space + "object" + else if (node.isTypeNode) + space + "type" + else + default + + /** + * Calls dot with a given dot string and returns the SVG output. + */ + private def generateSVG(dotInput: String, template: DocTemplateEntity) = { + val dotOutput = DiagramGenerator.getDotRunner().feedToDot(dotInput, template) + var tSVG = -System.currentTimeMillis + + val result = if (dotOutput != null) { + val src = scala.io.Source.fromString(dotOutput) + try { + val cpa = scala.xml.parsing.ConstructingParser.fromSource(src, preserveWS = false) + val doc = cpa.document() + if (doc != null) + transform(doc.docElem) + else + NodeSeq.Empty + } catch { + case exc: Exception => + if (settings.docDiagramsDebug.value) { + settings.printMsg("\n\n**********************************************************************") + settings.printMsg("Encountered an error while generating page for " + template.qualifiedName) + settings.printMsg(dotInput.toString.split("\n").mkString("\nDot input:\n\t","\n\t","")) + settings.printMsg(dotOutput.toString.split("\n").mkString("\nDot output:\n\t","\n\t","")) + settings.printMsg(exc.getStackTrace.mkString("\nException: " + exc.toString + ":\n\tat ", "\n\tat ","")) + settings.printMsg("\n\n**********************************************************************") + } else { + settings.printMsg("\nThe diagram for " + template.qualifiedName + " could not be created due to an internal error.") + settings.printMsg("Use " + settings.docDiagramsDebug.name + " for more information and please file this as a bug.") + } + NodeSeq.Empty + } + } else + NodeSeq.Empty + + tSVG += System.currentTimeMillis + DiagramStats.addSvgTime(tSVG) + + result + } + + /** + * Transforms the SVG generated by dot: + * - adds a class attribute to the SVG element + * - changes the path of the node images from absolute to relative + * - assigns id and class attributes to nodes and edges + * - removes title elements + */ + private def transform(e:scala.xml.Node): scala.xml.Node = e match { + // add an id and class attribute to the SVG element + case Elem(prefix, "svg", attribs, scope, child @ _*) => { + val klass = if (isInheritanceDiagram) "class-diagram" else "package-diagram" + Elem(prefix, "svg", attribs, scope, child map(x => transform(x)) : _*) % + new UnprefixedAttribute("id", "graph" + counter, Null) % + new UnprefixedAttribute("class", klass, Null) + } + // change the path of the node images from absolute to relative + case img @ <image></image> => { + val href = (img \ "@{http://www.w3.org/1999/xlink}href").toString + val file = href.substring(href.lastIndexOf("/") + 1, href.size) + img.asInstanceOf[Elem] % + new PrefixedAttribute("xlink", "href", pathToLib + file, Null) + } + // assign id and class attributes to edges and nodes: + // the id attribute generated by dot has the format: "{class}|{id}" + case g @ Elem(prefix, "g", attribs, scope, children @ _*) if (List("edge", "node").contains((g \ "@class").toString)) => { + var res = new Elem(prefix, "g", attribs, scope, (children map(x => transform(x))): _*) + val dotId = (g \ "@id").toString + if (dotId.count(_ == '|') == 1) { + val Array(klass, id) = dotId.toString.split("\\|") + /* Sometimes dot "forgets" to add the image -- that's very annoying, but it seems pretty random, and simple + * tests like excute 20K times and diff the output don't trigger the bug -- so it's up to us to place the image + * back in the node */ + val kind = getKind(klass) + if (kind != "") + if (((g \ "a" \ "image").isEmpty)) { + DiagramStats.addBrokenImage() + val xposition = getPosition(g, "x", -22) + val yposition = getPosition(g, "y", -11.3334) + if (xposition.isDefined && yposition.isDefined) { + val imageNode = <image xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href={ ("./lib/" + kind + "_diagram.png") } width="16px" height="16px" preserveAspectRatio="xMinYMin meet" x={ xposition.get.toString } y={ yposition.get.toString }/> + val anchorNode = (g \ "a") match { + case Seq(Elem(prefix, "a", attribs, scope, children @ _*)) => + transform(new Elem(prefix, "a", attribs, scope, (children ++ imageNode): _*)) + case _ => + g \ "a" + } + res = new Elem(prefix, "g", attribs, scope, anchorNode: _*) + DiagramStats.addFixedImage() + } + } + res % new UnprefixedAttribute("id", id, Null) % + new UnprefixedAttribute("class", (g \ "@class").toString + " " + klass, Null) + } + else res + } + // remove titles + case <title>{ _* }</title> => + scala.xml.Text("") + // apply recursively + case Elem(prefix, label, attribs, scope, child @ _*) => + Elem(prefix, label, attribs, scope, child map(x => transform(x)) : _*) + case x => x + } + + def getKind(klass: String): String = + if (klass.contains("class")) "class" + else if (klass.contains("trait")) "trait" + else if (klass.contains("object")) "object" + else "" + + def getPosition(g: scala.xml.Node, axis: String, offset: Double): Option[Double] = { + val node = g \ "a" \ "text" \ ("@" + axis) + if (node.isEmpty) + None + else + Some(node.toString.toDouble + offset) + } + + /* graph / node / edge attributes */ + + private val graphAttributes: Map[String, String] = Map( + "compound" -> "true", + "rankdir" -> "TB" + ) + + private val nodeAttributes = Map( + "shape" -> "rectangle", + "style" -> "filled", + "penwidth" -> "1", + "margin" -> "0.08,0.01", + "width" -> "0.0", + "height" -> "0.0", + "fontname" -> "Arial", + "fontsize" -> "10.00" + ) + + private val edgeAttributes = Map( + "color" -> "#d4d4d4", + "arrowsize" -> "0.5", + "fontcolor" -> "#aaaaaa", + "fontsize" -> "10.00", + "fontname" -> "Arial" + ) + + private val defaultStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d", + "margin" -> "0.1,0.04" + ) + + private val implicitStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d" + ) + + private val outsideStyle = Map( + "color" -> "#ababab", + "fillcolor" -> "#e1e1e1", + "fontcolor" -> "#7d7d7d" + ) + + private val traitStyle = Map( + "color" -> "#37657D", + "fillcolor" -> "#498AAD", + "fontcolor" -> "#ffffff" + ) + + private val classStyle = Map( + "color" -> "#115F3B", + "fillcolor" -> "#0A955B", + "fontcolor" -> "#ffffff" + ) + + private val objectStyle = Map( + "color" -> "#102966", + "fillcolor" -> "#3556a7", + "fontcolor" -> "#ffffff" + ) + + private val typeStyle = Map( + "color" -> "#115F3B", + "fillcolor" -> "#0A955B", + "fontcolor" -> "#ffffff" + ) + + private def flatten(attributes: Map[String, String]) = attributes.map{ case (key, value) => key + "=\"" + value + "\"" }.mkString(", ") + + private val graphAttributesStr = graphAttributes.map{ case (key, value) => key + "=\"" + value + "\";\n" }.mkString + private val nodeAttributesStr = flatten(nodeAttributes) + private val edgeAttributesStr = flatten(edgeAttributes) +} diff --git a/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala new file mode 100644 index 0000000000..2fa1bf62f3 --- /dev/null +++ b/src/scaladoc/scala/tools/nsc/doc/html/page/diagram/DotRunner.scala @@ -0,0 +1,225 @@ +package scala.tools.nsc +package doc +package html +package page +package diagram + +import java.io.InputStream +import java.io.OutputStream +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.io.BufferedWriter +import java.io.BufferedReader +import scala.sys.process._ +import scala.concurrent.SyncVar + +import model._ + +/** This class takes care of running the graphviz dot utility */ +class DotRunner(settings: doc.Settings) { + + private[this] var dotRestarts = 0 + private[this] var dotProcess: DotProcess = null + + def feedToDot(dotInput: String, template: DocTemplateEntity): String = { + + if (dotProcess == null) { + if (dotRestarts < settings.docDiagramsDotRestart.value) { + if (dotRestarts != 0) + settings.printMsg("Graphviz will be restarted...\n") + dotRestarts += 1 + dotProcess = new DotProcess(settings) + } else + return null + } + + val tStart = System.currentTimeMillis + val result = dotProcess.feedToDot(dotInput, template.qualifiedName) + val tFinish = System.currentTimeMillis + DiagramStats.addDotRunningTime(tFinish - tStart) + + if (result == null) { + dotProcess.cleanup() + dotProcess = null + if (dotRestarts == settings.docDiagramsDotRestart.value) { + settings.printMsg("\n") + settings.printMsg("**********************************************************************") + settings.printMsg("Diagrams will be disabled for this run because the graphviz dot tool") + settings.printMsg("has malfunctioned too many times. These scaladoc flags may help:") + settings.printMsg("") + val baseList = List(settings.docDiagramsDebug, + settings.docDiagramsDotPath, + settings.docDiagramsDotRestart, + settings.docDiagramsDotTimeout) + val width = (baseList map (_.helpSyntax.length)).max + def helpStr(s: doc.Settings#Setting) = ("%-" + width + "s") format (s.helpSyntax) + " " + s.helpDescription + baseList.foreach((sett: doc.Settings#Setting) => settings.printMsg(helpStr(sett))) + settings.printMsg("\nPlease note that graphviz package version 2.26 or above is required.") + settings.printMsg("**********************************************************************\n\n") + + } + } + + result + } + + def cleanup() = + if (dotProcess != null) + dotProcess.cleanup() +} + +class DotProcess(settings: doc.Settings) { + + @volatile var error: Boolean = false // signal an error + val inputString = new SyncVar[String] // used for the dot process input + val outputString = new SyncVar[String] // used for the dot process output + val errorBuffer: StringBuffer = new StringBuffer() // buffer used for both dot process error console AND logging + + // set in only one place, in the main thread + var process: Process = null + var templateName: String = "" + var templateInput: String = "" + + def feedToDot(input: String, template: String): String = { + + templateName = template + templateInput = input + + try { + + // process creation + if (process == null) { + val procIO = new ProcessIO(inputFn(_), outputFn(_), errorFn(_)) + val processBuilder: ProcessBuilder = Seq(settings.docDiagramsDotPath.value, "-Tsvg") + process = processBuilder.run(procIO) + } + + // pass the input and wait for the output + assert(!inputString.isSet) + assert(!outputString.isSet) + inputString.put(input) + var result = outputString.take(settings.docDiagramsDotTimeout.value * 1000) + if (error) result = null + + result + + } catch { + case exc: Throwable => + errorBuffer.append(" Main thread in " + templateName + ": " + + (if (exc.isInstanceOf[NoSuchElementException]) "Timeout" else "Exception: " + exc)) + error = true + return null + } + } + + def cleanup(): Unit = { + + // we'll need to know if there was any error for reporting + val _error = error + + if (process != null) { + // if there's no error, this should exit cleanly + if (!error) feedToDot("<finish>", "<finishing>") + + // just in case there's any thread hanging, this will take it out of the loop + error = true + process.destroy() + // we'll need to unblock the input again + if (!inputString.isSet) inputString.put("") + if (outputString.isSet) outputString.take() + } + + if (_error) { + if (settings.docDiagramsDebug.value) { + settings.printMsg("\n**********************************************************************") + settings.printMsg("The graphviz dot diagram tool has malfunctioned and will be restarted.") + settings.printMsg("\nThe following is the log of the failure:") + settings.printMsg(errorBuffer.toString) + settings.printMsg(" Cleanup: Last template: " + templateName) + settings.printMsg(" Cleanup: Last dot input: \n " + templateInput.replaceAll("\n","\n ") + "\n") + settings.printMsg(" Cleanup: Dot path: " + settings.docDiagramsDotPath.value) + if (process != null) + settings.printMsg(" Cleanup: Dot exit code: " + process.exitValue) + settings.printMsg("**********************************************************************") + } else { + // we shouldn't just sit there for 50s not reporting anything, no? + settings.printMsg("Graphviz dot encountered an error when generating the diagram for:") + settings.printMsg(templateName) + settings.printMsg("These are usually spurious errors, but if you notice a persistant error on") + settings.printMsg("a diagram, please use the " + settings.docDiagramsDebug.name + " flag and report a bug with the output.") + } + } + } + + /* The standard input passing function */ + private[this] def inputFn(stdin: OutputStream): Unit = { + val writer = new BufferedWriter(new OutputStreamWriter(stdin)) + try { + var input = inputString.take() + + while (!error) { + if (input == "<finish>") { + // empty => signal to finish + stdin.close() + return + } else { + // send output to dot + writer.write(input + "\n\n") + writer.flush() + } + + if (!error) input = inputString.take() + } + stdin.close() + } catch { + case exc: Throwable => + error = true + stdin.close() + errorBuffer.append(" Input thread in " + templateName + ": Exception: " + exc + "\n") + } + } + + private[this] def outputFn(stdOut: InputStream): Unit = { + val reader = new BufferedReader(new InputStreamReader(stdOut)) + val buffer: StringBuilder = new StringBuilder() + try { + var line = reader.readLine + while (!error && line != null) { + buffer.append(line + "\n") + // signal the last element in the svg (only for output) + if (line == "</svg>") { + outputString.put(buffer.toString) + buffer.setLength(0) + } + if (error) { stdOut.close(); return } + line = reader.readLine + } + assert(!outputString.isSet) + outputString.put(buffer.toString) + stdOut.close() + } catch { + case exc: Throwable => + error = true + stdOut.close() + errorBuffer.append(" Output thread in " + templateName + ": Exception: " + exc + "\n") + } + } + + private[this] def errorFn(stdErr: InputStream): Unit = { + val reader = new BufferedReader(new InputStreamReader(stdErr)) + try { + var line = reader.readLine + while (line != null) { + errorBuffer.append(" DOT <error console>: " + line + "\n") + error = true + line = reader.readLine + } + stdErr.close() + } catch { + case exc: Throwable => + error = true + stdErr.close() + errorBuffer.append(" Error thread in " + templateName + ": Exception: " + exc + "\n") + } + } +} |